machinaos 0.0.1 → 0.0.7

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 (422) hide show
  1. package/.env.template +71 -71
  2. package/LICENSE +21 -21
  3. package/README.md +163 -87
  4. package/bin/cli.js +62 -106
  5. package/client/.dockerignore +45 -45
  6. package/client/Dockerfile +68 -68
  7. package/client/dist/assets/index-DFSC53FP.css +1 -0
  8. package/client/dist/assets/index-fJ-1gTf5.js +613 -0
  9. package/client/dist/index.html +14 -0
  10. package/client/eslint.config.js +34 -16
  11. package/client/nginx.conf +66 -66
  12. package/client/package.json +61 -48
  13. package/client/src/App.tsx +27 -27
  14. package/client/src/Dashboard.tsx +1200 -1172
  15. package/client/src/ParameterPanel.tsx +302 -300
  16. package/client/src/components/AIAgentNode.tsx +315 -321
  17. package/client/src/components/APIKeyValidator.tsx +117 -117
  18. package/client/src/components/ClaudeChatModelNode.tsx +17 -17
  19. package/client/src/components/CredentialsModal.tsx +1200 -306
  20. package/client/src/components/GeminiChatModelNode.tsx +17 -17
  21. package/client/src/components/GenericNode.tsx +356 -356
  22. package/client/src/components/LocationParameterPanel.tsx +153 -153
  23. package/client/src/components/ModelNode.tsx +285 -285
  24. package/client/src/components/OpenAIChatModelNode.tsx +17 -17
  25. package/client/src/components/OutputPanel.tsx +470 -470
  26. package/client/src/components/ParameterRenderer.tsx +1873 -1873
  27. package/client/src/components/SkillEditorModal.tsx +3 -3
  28. package/client/src/components/SquareNode.tsx +812 -796
  29. package/client/src/components/ToolkitNode.tsx +365 -365
  30. package/client/src/components/auth/LoginPage.tsx +247 -247
  31. package/client/src/components/auth/ProtectedRoute.tsx +59 -59
  32. package/client/src/components/base/BaseChatModelNode.tsx +270 -270
  33. package/client/src/components/icons/AIProviderIcons.tsx +50 -50
  34. package/client/src/components/maps/GoogleMapsPicker.tsx +136 -136
  35. package/client/src/components/maps/MapsPreviewPanel.tsx +109 -109
  36. package/client/src/components/maps/index.ts +25 -25
  37. package/client/src/components/parameterPanel/InputSection.tsx +1094 -1094
  38. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +64 -64
  39. package/client/src/components/parameterPanel/MapsSection.tsx +91 -91
  40. package/client/src/components/parameterPanel/MiddleSection.tsx +867 -571
  41. package/client/src/components/parameterPanel/OutputSection.tsx +80 -80
  42. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +81 -81
  43. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -436
  44. package/client/src/components/parameterPanel/index.ts +41 -41
  45. package/client/src/components/shared/DataPanel.tsx +142 -142
  46. package/client/src/components/shared/JSONTreeRenderer.tsx +105 -105
  47. package/client/src/components/ui/AIResultModal.tsx +203 -203
  48. package/client/src/components/ui/ApiKeyInput.tsx +93 -0
  49. package/client/src/components/ui/CodeEditor.tsx +81 -81
  50. package/client/src/components/ui/CollapsibleSection.tsx +87 -87
  51. package/client/src/components/ui/ComponentItem.tsx +153 -153
  52. package/client/src/components/ui/ComponentPalette.tsx +320 -320
  53. package/client/src/components/ui/ConsolePanel.tsx +151 -43
  54. package/client/src/components/ui/ErrorBoundary.tsx +195 -195
  55. package/client/src/components/ui/InputNodesPanel.tsx +203 -203
  56. package/client/src/components/ui/MapSelector.tsx +313 -313
  57. package/client/src/components/ui/Modal.tsx +151 -148
  58. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -1150
  59. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -381
  60. package/client/src/components/ui/QRCodeDisplay.tsx +182 -0
  61. package/client/src/components/ui/TopToolbar.tsx +736 -736
  62. package/client/src/components/ui/WorkflowSidebar.tsx +293 -293
  63. package/client/src/config/antdTheme.ts +186 -186
  64. package/client/src/contexts/AuthContext.tsx +221 -221
  65. package/client/src/contexts/ThemeContext.tsx +42 -42
  66. package/client/src/contexts/WebSocketContext.tsx +2144 -1971
  67. package/client/src/factories/baseChatModelFactory.ts +255 -255
  68. package/client/src/hooks/useAndroidOperations.ts +118 -164
  69. package/client/src/hooks/useApiKeyValidation.ts +106 -106
  70. package/client/src/hooks/useApiKeys.ts +238 -238
  71. package/client/src/hooks/useAppTheme.ts +17 -17
  72. package/client/src/hooks/useComponentPalette.ts +50 -50
  73. package/client/src/hooks/useDragAndDrop.ts +123 -123
  74. package/client/src/hooks/useDragVariable.ts +88 -88
  75. package/client/src/hooks/useExecution.ts +319 -313
  76. package/client/src/hooks/useParameterPanel.ts +176 -176
  77. package/client/src/hooks/useReactFlowNodes.ts +188 -188
  78. package/client/src/hooks/useToolSchema.ts +209 -209
  79. package/client/src/hooks/useWhatsApp.ts +196 -196
  80. package/client/src/hooks/useWorkflowManagement.ts +45 -45
  81. package/client/src/index.css +314 -314
  82. package/client/src/nodeDefinitions/aiAgentNodes.ts +335 -335
  83. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -340
  84. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -383
  85. package/client/src/nodeDefinitions/chatNodes.ts +135 -135
  86. package/client/src/nodeDefinitions/codeNodes.ts +54 -54
  87. package/client/src/nodeDefinitions/index.ts +14 -14
  88. package/client/src/nodeDefinitions/locationNodes.ts +462 -462
  89. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -220
  90. package/client/src/nodeDefinitions/skillNodes.ts +17 -5
  91. package/client/src/nodeDefinitions/utilityNodes.ts +284 -284
  92. package/client/src/nodeDefinitions/whatsappNodes.ts +821 -865
  93. package/client/src/nodeDefinitions.ts +101 -103
  94. package/client/src/services/dynamicParameterService.ts +95 -95
  95. package/client/src/services/execution/aiAgentExecutionService.ts +34 -34
  96. package/client/src/services/executionService.ts +227 -231
  97. package/client/src/services/workflowApi.ts +91 -91
  98. package/client/src/store/useAppStore.ts +578 -581
  99. package/client/src/styles/theme.ts +513 -508
  100. package/client/src/styles/zIndex.ts +16 -16
  101. package/client/src/types/ComponentTypes.ts +38 -38
  102. package/client/src/types/INodeProperties.ts +287 -287
  103. package/client/src/types/NodeTypes.ts +27 -27
  104. package/client/src/utils/formatters.ts +32 -32
  105. package/client/src/utils/googleMapsLoader.ts +139 -139
  106. package/client/src/utils/locationUtils.ts +84 -84
  107. package/client/src/utils/nodeUtils.ts +30 -30
  108. package/client/src/utils/workflow.ts +29 -29
  109. package/client/src/vite-env.d.ts +12 -12
  110. package/client/tailwind.config.js +59 -59
  111. package/client/tsconfig.json +25 -25
  112. package/client/vite.config.js +35 -35
  113. package/install.ps1 +308 -0
  114. package/install.sh +343 -0
  115. package/package.json +81 -70
  116. package/scripts/build.js +174 -51
  117. package/scripts/clean.js +40 -40
  118. package/scripts/start.js +234 -210
  119. package/scripts/stop.js +301 -325
  120. package/server/.dockerignore +44 -44
  121. package/server/Dockerfile +45 -45
  122. package/server/constants.py +244 -249
  123. package/server/core/cache.py +460 -460
  124. package/server/core/config.py +127 -127
  125. package/server/core/container.py +98 -98
  126. package/server/core/database.py +1296 -1210
  127. package/server/core/logging.py +313 -313
  128. package/server/main.py +288 -288
  129. package/server/middleware/__init__.py +5 -5
  130. package/server/middleware/auth.py +89 -89
  131. package/server/models/auth.py +52 -52
  132. package/server/models/cache.py +24 -24
  133. package/server/models/database.py +235 -210
  134. package/server/models/nodes.py +435 -455
  135. package/server/pyproject.toml +75 -72
  136. package/server/requirements.txt +83 -83
  137. package/server/routers/android.py +294 -294
  138. package/server/routers/auth.py +203 -203
  139. package/server/routers/database.py +150 -150
  140. package/server/routers/maps.py +141 -141
  141. package/server/routers/nodejs_compat.py +288 -288
  142. package/server/routers/webhook.py +90 -90
  143. package/server/routers/websocket.py +2239 -2127
  144. package/server/routers/whatsapp.py +761 -761
  145. package/server/routers/workflow.py +199 -199
  146. package/server/services/ai.py +2444 -2414
  147. package/server/services/android_service.py +588 -588
  148. package/server/services/auth.py +130 -130
  149. package/server/services/chat_client.py +160 -160
  150. package/server/services/deployment/manager.py +706 -706
  151. package/server/services/event_waiter.py +675 -785
  152. package/server/services/execution/executor.py +1351 -1351
  153. package/server/services/execution/models.py +1 -1
  154. package/server/services/handlers/__init__.py +122 -126
  155. package/server/services/handlers/ai.py +390 -355
  156. package/server/services/handlers/android.py +69 -260
  157. package/server/services/handlers/code.py +278 -278
  158. package/server/services/handlers/http.py +193 -193
  159. package/server/services/handlers/tools.py +146 -32
  160. package/server/services/handlers/triggers.py +107 -107
  161. package/server/services/handlers/utility.py +822 -822
  162. package/server/services/handlers/whatsapp.py +423 -476
  163. package/server/services/maps.py +288 -288
  164. package/server/services/memory_store.py +103 -103
  165. package/server/services/node_executor.py +372 -375
  166. package/server/services/scheduler.py +155 -155
  167. package/server/services/skill_loader.py +1 -1
  168. package/server/services/status_broadcaster.py +834 -826
  169. package/server/services/temporal/__init__.py +23 -23
  170. package/server/services/temporal/activities.py +344 -344
  171. package/server/services/temporal/client.py +76 -76
  172. package/server/services/temporal/executor.py +147 -147
  173. package/server/services/temporal/worker.py +251 -251
  174. package/server/services/temporal/workflow.py +355 -355
  175. package/server/services/temporal/ws_client.py +236 -236
  176. package/server/services/text.py +110 -110
  177. package/server/services/user_auth.py +172 -172
  178. package/server/services/websocket_client.py +29 -29
  179. package/server/services/workflow.py +597 -597
  180. package/server/skills/android-skill/SKILL.md +4 -4
  181. package/server/skills/code-skill/SKILL.md +123 -89
  182. package/server/skills/maps-skill/SKILL.md +3 -3
  183. package/server/skills/memory-skill/SKILL.md +1 -1
  184. package/server/skills/web-search-skill/SKILL.md +154 -0
  185. package/server/skills/whatsapp-skill/SKILL.md +3 -3
  186. package/server/uv.lock +461 -100
  187. package/server/whatsapp-rpc/.dockerignore +30 -30
  188. package/server/whatsapp-rpc/Dockerfile +44 -44
  189. package/server/whatsapp-rpc/Dockerfile.web +17 -17
  190. package/server/whatsapp-rpc/README.md +139 -139
  191. package/server/whatsapp-rpc/bin/whatsapp-rpc-server +0 -0
  192. package/server/whatsapp-rpc/cli.js +95 -95
  193. package/server/whatsapp-rpc/configs/config.yaml +6 -6
  194. package/server/whatsapp-rpc/docker-compose.yml +35 -35
  195. package/server/whatsapp-rpc/docs/API.md +410 -410
  196. package/server/whatsapp-rpc/node_modules/.package-lock.json +259 -0
  197. package/server/whatsapp-rpc/node_modules/chalk/license +9 -0
  198. package/server/whatsapp-rpc/node_modules/chalk/package.json +83 -0
  199. package/server/whatsapp-rpc/node_modules/chalk/readme.md +297 -0
  200. package/server/whatsapp-rpc/node_modules/chalk/source/index.d.ts +325 -0
  201. package/server/whatsapp-rpc/node_modules/chalk/source/index.js +225 -0
  202. package/server/whatsapp-rpc/node_modules/chalk/source/utilities.js +33 -0
  203. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  204. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  205. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  206. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  207. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  208. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  209. package/server/whatsapp-rpc/node_modules/commander/LICENSE +22 -0
  210. package/server/whatsapp-rpc/node_modules/commander/Readme.md +1148 -0
  211. package/server/whatsapp-rpc/node_modules/commander/esm.mjs +16 -0
  212. package/server/whatsapp-rpc/node_modules/commander/index.js +26 -0
  213. package/server/whatsapp-rpc/node_modules/commander/lib/argument.js +145 -0
  214. package/server/whatsapp-rpc/node_modules/commander/lib/command.js +2179 -0
  215. package/server/whatsapp-rpc/node_modules/commander/lib/error.js +43 -0
  216. package/server/whatsapp-rpc/node_modules/commander/lib/help.js +462 -0
  217. package/server/whatsapp-rpc/node_modules/commander/lib/option.js +329 -0
  218. package/server/whatsapp-rpc/node_modules/commander/lib/suggestSimilar.js +100 -0
  219. package/server/whatsapp-rpc/node_modules/commander/package-support.json +16 -0
  220. package/server/whatsapp-rpc/node_modules/commander/package.json +80 -0
  221. package/server/whatsapp-rpc/node_modules/commander/typings/esm.d.mts +3 -0
  222. package/server/whatsapp-rpc/node_modules/commander/typings/index.d.ts +884 -0
  223. package/server/whatsapp-rpc/node_modules/cross-spawn/LICENSE +21 -0
  224. package/server/whatsapp-rpc/node_modules/cross-spawn/README.md +89 -0
  225. package/server/whatsapp-rpc/node_modules/cross-spawn/index.js +39 -0
  226. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/enoent.js +59 -0
  227. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/parse.js +91 -0
  228. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/escape.js +47 -0
  229. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/readShebang.js +23 -0
  230. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/resolveCommand.js +52 -0
  231. package/server/whatsapp-rpc/node_modules/cross-spawn/package.json +73 -0
  232. package/server/whatsapp-rpc/node_modules/execa/index.d.ts +955 -0
  233. package/server/whatsapp-rpc/node_modules/execa/index.js +309 -0
  234. package/server/whatsapp-rpc/node_modules/execa/lib/command.js +119 -0
  235. package/server/whatsapp-rpc/node_modules/execa/lib/error.js +87 -0
  236. package/server/whatsapp-rpc/node_modules/execa/lib/kill.js +102 -0
  237. package/server/whatsapp-rpc/node_modules/execa/lib/pipe.js +42 -0
  238. package/server/whatsapp-rpc/node_modules/execa/lib/promise.js +36 -0
  239. package/server/whatsapp-rpc/node_modules/execa/lib/stdio.js +49 -0
  240. package/server/whatsapp-rpc/node_modules/execa/lib/stream.js +133 -0
  241. package/server/whatsapp-rpc/node_modules/execa/lib/verbose.js +19 -0
  242. package/server/whatsapp-rpc/node_modules/execa/license +9 -0
  243. package/server/whatsapp-rpc/node_modules/execa/package.json +90 -0
  244. package/server/whatsapp-rpc/node_modules/execa/readme.md +822 -0
  245. package/server/whatsapp-rpc/node_modules/get-stream/license +9 -0
  246. package/server/whatsapp-rpc/node_modules/get-stream/package.json +53 -0
  247. package/server/whatsapp-rpc/node_modules/get-stream/readme.md +291 -0
  248. package/server/whatsapp-rpc/node_modules/get-stream/source/array-buffer.js +84 -0
  249. package/server/whatsapp-rpc/node_modules/get-stream/source/array.js +32 -0
  250. package/server/whatsapp-rpc/node_modules/get-stream/source/buffer.js +20 -0
  251. package/server/whatsapp-rpc/node_modules/get-stream/source/contents.js +101 -0
  252. package/server/whatsapp-rpc/node_modules/get-stream/source/index.d.ts +119 -0
  253. package/server/whatsapp-rpc/node_modules/get-stream/source/index.js +5 -0
  254. package/server/whatsapp-rpc/node_modules/get-stream/source/string.js +36 -0
  255. package/server/whatsapp-rpc/node_modules/get-stream/source/utils.js +11 -0
  256. package/server/whatsapp-rpc/node_modules/get-them-args/LICENSE +21 -0
  257. package/server/whatsapp-rpc/node_modules/get-them-args/README.md +95 -0
  258. package/server/whatsapp-rpc/node_modules/get-them-args/index.js +97 -0
  259. package/server/whatsapp-rpc/node_modules/get-them-args/package.json +36 -0
  260. package/server/whatsapp-rpc/node_modules/human-signals/LICENSE +201 -0
  261. package/server/whatsapp-rpc/node_modules/human-signals/README.md +168 -0
  262. package/server/whatsapp-rpc/node_modules/human-signals/build/src/core.js +273 -0
  263. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.d.ts +73 -0
  264. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.js +70 -0
  265. package/server/whatsapp-rpc/node_modules/human-signals/build/src/realtime.js +16 -0
  266. package/server/whatsapp-rpc/node_modules/human-signals/build/src/signals.js +34 -0
  267. package/server/whatsapp-rpc/node_modules/human-signals/package.json +61 -0
  268. package/server/whatsapp-rpc/node_modules/is-stream/index.d.ts +81 -0
  269. package/server/whatsapp-rpc/node_modules/is-stream/index.js +29 -0
  270. package/server/whatsapp-rpc/node_modules/is-stream/license +9 -0
  271. package/server/whatsapp-rpc/node_modules/is-stream/package.json +44 -0
  272. package/server/whatsapp-rpc/node_modules/is-stream/readme.md +60 -0
  273. package/server/whatsapp-rpc/node_modules/isexe/LICENSE +15 -0
  274. package/server/whatsapp-rpc/node_modules/isexe/README.md +51 -0
  275. package/server/whatsapp-rpc/node_modules/isexe/index.js +57 -0
  276. package/server/whatsapp-rpc/node_modules/isexe/mode.js +41 -0
  277. package/server/whatsapp-rpc/node_modules/isexe/package.json +31 -0
  278. package/server/whatsapp-rpc/node_modules/isexe/test/basic.js +221 -0
  279. package/server/whatsapp-rpc/node_modules/isexe/windows.js +42 -0
  280. package/server/whatsapp-rpc/node_modules/kill-port/.editorconfig +12 -0
  281. package/server/whatsapp-rpc/node_modules/kill-port/.gitattributes +1 -0
  282. package/server/whatsapp-rpc/node_modules/kill-port/LICENSE +21 -0
  283. package/server/whatsapp-rpc/node_modules/kill-port/README.md +140 -0
  284. package/server/whatsapp-rpc/node_modules/kill-port/cli.js +25 -0
  285. package/server/whatsapp-rpc/node_modules/kill-port/example.js +21 -0
  286. package/server/whatsapp-rpc/node_modules/kill-port/index.js +46 -0
  287. package/server/whatsapp-rpc/node_modules/kill-port/logo.png +0 -0
  288. package/server/whatsapp-rpc/node_modules/kill-port/package.json +41 -0
  289. package/server/whatsapp-rpc/node_modules/kill-port/pnpm-lock.yaml +4606 -0
  290. package/server/whatsapp-rpc/node_modules/kill-port/test.js +16 -0
  291. package/server/whatsapp-rpc/node_modules/merge-stream/LICENSE +21 -0
  292. package/server/whatsapp-rpc/node_modules/merge-stream/README.md +78 -0
  293. package/server/whatsapp-rpc/node_modules/merge-stream/index.js +41 -0
  294. package/server/whatsapp-rpc/node_modules/merge-stream/package.json +19 -0
  295. package/server/whatsapp-rpc/node_modules/mimic-fn/index.d.ts +52 -0
  296. package/server/whatsapp-rpc/node_modules/mimic-fn/index.js +71 -0
  297. package/server/whatsapp-rpc/node_modules/mimic-fn/license +9 -0
  298. package/server/whatsapp-rpc/node_modules/mimic-fn/package.json +45 -0
  299. package/server/whatsapp-rpc/node_modules/mimic-fn/readme.md +90 -0
  300. package/server/whatsapp-rpc/node_modules/npm-run-path/index.d.ts +90 -0
  301. package/server/whatsapp-rpc/node_modules/npm-run-path/index.js +52 -0
  302. package/server/whatsapp-rpc/node_modules/npm-run-path/license +9 -0
  303. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.d.ts +31 -0
  304. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.js +12 -0
  305. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/license +9 -0
  306. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/package.json +41 -0
  307. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/readme.md +57 -0
  308. package/server/whatsapp-rpc/node_modules/npm-run-path/package.json +49 -0
  309. package/server/whatsapp-rpc/node_modules/npm-run-path/readme.md +104 -0
  310. package/server/whatsapp-rpc/node_modules/onetime/index.d.ts +59 -0
  311. package/server/whatsapp-rpc/node_modules/onetime/index.js +41 -0
  312. package/server/whatsapp-rpc/node_modules/onetime/license +9 -0
  313. package/server/whatsapp-rpc/node_modules/onetime/package.json +45 -0
  314. package/server/whatsapp-rpc/node_modules/onetime/readme.md +94 -0
  315. package/server/whatsapp-rpc/node_modules/path-key/index.d.ts +40 -0
  316. package/server/whatsapp-rpc/node_modules/path-key/index.js +16 -0
  317. package/server/whatsapp-rpc/node_modules/path-key/license +9 -0
  318. package/server/whatsapp-rpc/node_modules/path-key/package.json +39 -0
  319. package/server/whatsapp-rpc/node_modules/path-key/readme.md +61 -0
  320. package/server/whatsapp-rpc/node_modules/shebang-command/index.js +19 -0
  321. package/server/whatsapp-rpc/node_modules/shebang-command/license +9 -0
  322. package/server/whatsapp-rpc/node_modules/shebang-command/package.json +34 -0
  323. package/server/whatsapp-rpc/node_modules/shebang-command/readme.md +34 -0
  324. package/server/whatsapp-rpc/node_modules/shebang-regex/index.d.ts +22 -0
  325. package/server/whatsapp-rpc/node_modules/shebang-regex/index.js +2 -0
  326. package/server/whatsapp-rpc/node_modules/shebang-regex/license +9 -0
  327. package/server/whatsapp-rpc/node_modules/shebang-regex/package.json +35 -0
  328. package/server/whatsapp-rpc/node_modules/shebang-regex/readme.md +33 -0
  329. package/server/whatsapp-rpc/node_modules/shell-exec/LICENSE +21 -0
  330. package/server/whatsapp-rpc/node_modules/shell-exec/README.md +60 -0
  331. package/server/whatsapp-rpc/node_modules/shell-exec/index.js +47 -0
  332. package/server/whatsapp-rpc/node_modules/shell-exec/package.json +29 -0
  333. package/server/whatsapp-rpc/node_modules/signal-exit/LICENSE.txt +16 -0
  334. package/server/whatsapp-rpc/node_modules/signal-exit/README.md +74 -0
  335. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts +12 -0
  336. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts.map +1 -0
  337. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js +10 -0
  338. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js.map +1 -0
  339. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts +48 -0
  340. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts.map +1 -0
  341. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js +279 -0
  342. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js.map +1 -0
  343. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/package.json +3 -0
  344. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts +29 -0
  345. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts.map +1 -0
  346. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js +42 -0
  347. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js.map +1 -0
  348. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts +12 -0
  349. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts.map +1 -0
  350. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js +4 -0
  351. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js.map +1 -0
  352. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts +48 -0
  353. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts.map +1 -0
  354. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js +275 -0
  355. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js.map +1 -0
  356. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/package.json +3 -0
  357. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts +29 -0
  358. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts.map +1 -0
  359. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js +39 -0
  360. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js.map +1 -0
  361. package/server/whatsapp-rpc/node_modules/signal-exit/package.json +106 -0
  362. package/server/whatsapp-rpc/node_modules/strip-final-newline/index.js +14 -0
  363. package/server/whatsapp-rpc/node_modules/strip-final-newline/license +9 -0
  364. package/server/whatsapp-rpc/node_modules/strip-final-newline/package.json +43 -0
  365. package/server/whatsapp-rpc/node_modules/strip-final-newline/readme.md +35 -0
  366. package/server/whatsapp-rpc/node_modules/which/CHANGELOG.md +166 -0
  367. package/server/whatsapp-rpc/node_modules/which/LICENSE +15 -0
  368. package/server/whatsapp-rpc/node_modules/which/README.md +54 -0
  369. package/server/whatsapp-rpc/node_modules/which/bin/node-which +52 -0
  370. package/server/whatsapp-rpc/node_modules/which/package.json +43 -0
  371. package/server/whatsapp-rpc/node_modules/which/which.js +125 -0
  372. package/server/whatsapp-rpc/package-lock.json +272 -0
  373. package/server/whatsapp-rpc/package.json +30 -30
  374. package/server/whatsapp-rpc/schema.json +1294 -1294
  375. package/server/whatsapp-rpc/scripts/clean.cjs +66 -66
  376. package/server/whatsapp-rpc/scripts/cli.js +162 -162
  377. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -166
  378. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -15
  379. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -4
  380. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -427
  381. package/server/whatsapp-rpc/web/app.py +609 -609
  382. package/server/whatsapp-rpc/web/requirements.txt +6 -6
  383. package/server/whatsapp-rpc/web/rpc_client.py +427 -427
  384. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -59
  385. package/server/whatsapp-rpc/web/templates/base.html +149 -149
  386. package/server/whatsapp-rpc/web/templates/contacts.html +240 -240
  387. package/server/whatsapp-rpc/web/templates/dashboard.html +319 -319
  388. package/server/whatsapp-rpc/web/templates/groups.html +328 -328
  389. package/server/whatsapp-rpc/web/templates/messages.html +465 -465
  390. package/server/whatsapp-rpc/web/templates/messaging.html +680 -680
  391. package/server/whatsapp-rpc/web/templates/send.html +258 -258
  392. package/server/whatsapp-rpc/web/templates/settings.html +459 -459
  393. package/client/src/components/ui/AndroidSettingsPanel.tsx +0 -401
  394. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +0 -345
  395. package/client/src/nodeDefinitions/androidDeviceNodes.ts +0 -140
  396. package/docker-compose.prod.yml +0 -107
  397. package/docker-compose.yml +0 -104
  398. package/docs-MachinaOs/README.md +0 -85
  399. package/docs-MachinaOs/deployment/docker.mdx +0 -228
  400. package/docs-MachinaOs/deployment/production.mdx +0 -345
  401. package/docs-MachinaOs/docs.json +0 -75
  402. package/docs-MachinaOs/faq.mdx +0 -309
  403. package/docs-MachinaOs/favicon.svg +0 -5
  404. package/docs-MachinaOs/installation.mdx +0 -160
  405. package/docs-MachinaOs/introduction.mdx +0 -114
  406. package/docs-MachinaOs/logo/dark.svg +0 -6
  407. package/docs-MachinaOs/logo/light.svg +0 -6
  408. package/docs-MachinaOs/nodes/ai-agent.mdx +0 -216
  409. package/docs-MachinaOs/nodes/ai-models.mdx +0 -240
  410. package/docs-MachinaOs/nodes/android.mdx +0 -411
  411. package/docs-MachinaOs/nodes/overview.mdx +0 -181
  412. package/docs-MachinaOs/nodes/schedulers.mdx +0 -316
  413. package/docs-MachinaOs/nodes/webhooks.mdx +0 -330
  414. package/docs-MachinaOs/nodes/whatsapp.mdx +0 -305
  415. package/docs-MachinaOs/quickstart.mdx +0 -119
  416. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +0 -177
  417. package/docs-MachinaOs/tutorials/android-automation.mdx +0 -242
  418. package/docs-MachinaOs/tutorials/first-workflow.mdx +0 -134
  419. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +0 -185
  420. package/nul +0 -0
  421. package/scripts/check-ports.ps1 +0 -33
  422. package/scripts/kill-port.ps1 +0 -154
@@ -1,597 +1,597 @@
1
- """Workflow Service - Facade for workflow execution and deployment.
2
-
3
- This is a thin facade that delegates to specialized modules:
4
- - NodeExecutor: Single node execution
5
- - ParameterResolver: Template variable resolution
6
- - DeploymentManager: Event-driven deployment lifecycle
7
- - WorkflowExecutor: Parallel/sequential orchestration
8
- - TemporalExecutor: Durable workflow execution (optional)
9
-
10
- Following n8n/Conductor patterns for clean separation of concerns.
11
- """
12
-
13
- import time
14
- from datetime import datetime
15
- from typing import Dict, Any, List, Optional, TYPE_CHECKING
16
-
17
- from core.logging import get_logger
18
- from constants import WORKFLOW_TRIGGER_TYPES
19
- from services.node_executor import NodeExecutor
20
- from services.parameter_resolver import ParameterResolver
21
- from services.deployment import DeploymentManager
22
- from services.execution import WorkflowExecutor, ExecutionCache
23
-
24
- if TYPE_CHECKING:
25
- from core.config import Settings
26
- from core.database import Database
27
- from core.cache import CacheService
28
- from services.ai import AIService
29
- from services.maps import MapsService
30
- from services.text import TextService
31
- from services.android_service import AndroidService
32
- from services.temporal import TemporalExecutor
33
-
34
- logger = get_logger(__name__)
35
-
36
-
37
- class WorkflowService:
38
- """Workflow execution and deployment service.
39
-
40
- Thin facade delegating to specialized modules for:
41
- - Node execution (NodeExecutor)
42
- - Parameter resolution (ParameterResolver)
43
- - Deployment lifecycle (DeploymentManager)
44
- - Workflow orchestration (WorkflowExecutor or TemporalExecutor)
45
- """
46
-
47
- def __init__(
48
- self,
49
- database: "Database",
50
- ai_service: "AIService",
51
- maps_service: "MapsService",
52
- text_service: "TextService",
53
- android_service: "AndroidService",
54
- cache: "CacheService",
55
- settings: "Settings",
56
- ):
57
- self.database = database
58
- self.settings = settings
59
-
60
- # In-memory output storage (fast access during execution)
61
- self._outputs: Dict[str, Dict[str, Any]] = {}
62
-
63
- # Initialize NodeExecutor
64
- self._node_executor = NodeExecutor(
65
- database=database,
66
- ai_service=ai_service,
67
- maps_service=maps_service,
68
- text_service=text_service,
69
- android_service=android_service,
70
- settings=settings,
71
- output_store=self.store_node_output,
72
- )
73
-
74
- # Initialize ParameterResolver
75
- self._param_resolver = ParameterResolver(
76
- database=database,
77
- get_output_fn=self.get_node_output,
78
- )
79
-
80
- # Initialize Execution Cache
81
- self._execution_cache = ExecutionCache(cache)
82
- self._workflow_executor: Optional[WorkflowExecutor] = None
83
-
84
- # Temporal executor (set via set_temporal_executor when enabled)
85
- self._temporal_executor: Optional["TemporalExecutor"] = None
86
-
87
- # Initialize DeploymentManager (lazy - needs broadcaster)
88
- self._deployment_manager: Optional[DeploymentManager] = None
89
- self._broadcaster = None
90
-
91
- # Deployment settings
92
- self._settings = {
93
- "stop_on_error": False,
94
- "max_concurrent_runs": 100,
95
- "use_parallel_executor": True,
96
- }
97
-
98
- def set_temporal_executor(self, executor: "TemporalExecutor") -> None:
99
- """Set the Temporal executor for durable workflow execution.
100
-
101
- Args:
102
- executor: Configured TemporalExecutor instance
103
- """
104
- self._temporal_executor = executor
105
- logger.info("Temporal executor configured for workflow execution")
106
-
107
- def _get_deployment_manager(self) -> DeploymentManager:
108
- """Get or create DeploymentManager."""
109
- if self._deployment_manager is None:
110
- from services.status_broadcaster import get_status_broadcaster
111
- self._broadcaster = get_status_broadcaster()
112
- self._deployment_manager = DeploymentManager(
113
- database=self.database,
114
- execute_workflow_fn=self.execute_workflow,
115
- store_output_fn=self.store_node_output,
116
- broadcaster=self._broadcaster,
117
- )
118
- return self._deployment_manager
119
-
120
- def _get_workflow_executor(self, status_callback=None) -> WorkflowExecutor:
121
- """Get or create WorkflowExecutor."""
122
- if self._workflow_executor is None or status_callback:
123
- self._workflow_executor = WorkflowExecutor(
124
- cache=self._execution_cache,
125
- node_executor=self._execute_node_adapter,
126
- status_callback=status_callback,
127
- dlq_enabled=self.settings.dlq_enabled,
128
- )
129
- return self._workflow_executor
130
-
131
- # =========================================================================
132
- # NODE EXECUTION
133
- # =========================================================================
134
-
135
- async def execute_node(
136
- self,
137
- node_id: str,
138
- node_type: str,
139
- parameters: Dict[str, Any],
140
- nodes: List[Dict] = None,
141
- edges: List[Dict] = None,
142
- session_id: str = "default",
143
- execution_id: str = None,
144
- workflow_id: str = None,
145
- ) -> Dict[str, Any]:
146
- """Execute a single workflow node."""
147
- context = {
148
- "nodes": nodes,
149
- "edges": edges,
150
- "session_id": session_id,
151
- "execution_id": execution_id,
152
- "workflow_id": workflow_id, # For per-workflow status scoping (n8n pattern)
153
- "get_output_fn": self.get_node_output,
154
- }
155
- return await self._node_executor.execute(
156
- node_id=node_id,
157
- node_type=node_type,
158
- parameters=parameters,
159
- context=context,
160
- resolve_params_fn=self._param_resolver.resolve,
161
- )
162
-
163
- async def _execute_node_adapter(
164
- self,
165
- node_id: str,
166
- node_type: str,
167
- parameters: Dict[str, Any],
168
- context: Dict[str, Any],
169
- ) -> Dict[str, Any]:
170
- """Adapter for WorkflowExecutor to call NodeExecutor."""
171
- return await self.execute_node(
172
- node_id=node_id,
173
- node_type=node_type,
174
- parameters=parameters,
175
- nodes=context.get("nodes"),
176
- edges=context.get("edges"),
177
- session_id=context.get("session_id", "default"),
178
- execution_id=context.get("execution_id"),
179
- workflow_id=context.get("workflow_id"), # Pass workflow_id for status scoping
180
- )
181
-
182
- # =========================================================================
183
- # WORKFLOW EXECUTION
184
- # =========================================================================
185
-
186
- async def execute_workflow(
187
- self,
188
- nodes: List[Dict],
189
- edges: List[Dict],
190
- session_id: str = "default",
191
- status_callback=None,
192
- use_parallel: bool = None,
193
- skip_clear_outputs: bool = False,
194
- workflow_id: Optional[str] = None,
195
- use_temporal: bool = None,
196
- ) -> Dict[str, Any]:
197
- """Execute entire workflow.
198
-
199
- Args:
200
- nodes: Workflow nodes
201
- edges: Workflow edges
202
- session_id: Session identifier
203
- status_callback: Status update callback
204
- use_parallel: Force parallel/sequential execution
205
- skip_clear_outputs: Skip clearing outputs (for deployment runs)
206
- workflow_id: Workflow ID for per-workflow status scoping (n8n pattern)
207
- use_temporal: Force Temporal execution (None = use settings default)
208
- """
209
- start_time = time.time()
210
-
211
- # Clear outputs unless skipped
212
- if not skip_clear_outputs:
213
- await self.clear_all_outputs(session_id)
214
-
215
- if not nodes:
216
- return self._error_result("No nodes in workflow", start_time)
217
-
218
- # Find start node
219
- start_node = self._find_start_node(nodes)
220
- if not start_node:
221
- return self._error_result("No start node found", start_time)
222
-
223
- # Determine execution mode
224
- if use_parallel is None:
225
- use_parallel = self._settings.get("use_parallel_executor", True)
226
-
227
- # Check if Temporal execution is requested
228
- if use_temporal is None:
229
- use_temporal = self.settings.temporal_enabled
230
-
231
- # Use Temporal if enabled and executor is configured
232
- if use_temporal and self._temporal_executor is not None:
233
- return await self._execute_temporal(nodes, edges, session_id, status_callback, start_time, workflow_id)
234
-
235
- # Log warning if Temporal was requested but not available
236
- if use_temporal and self._temporal_executor is None:
237
- logger.warning(
238
- "Temporal execution requested but executor not configured. "
239
- "Falling back to parallel/sequential execution. "
240
- "Check TEMPORAL_ENABLED and Temporal server connection."
241
- )
242
-
243
- # Use parallel executor if enabled and Redis available
244
- if use_parallel and self.settings.redis_enabled:
245
- return await self._execute_parallel(nodes, edges, session_id, status_callback, start_time, workflow_id)
246
-
247
- # Fall back to sequential
248
- return await self._execute_sequential(nodes, edges, session_id, status_callback, start_time, workflow_id)
249
-
250
- async def _execute_temporal(self, nodes, edges, session_id, status_callback, start_time, workflow_id: Optional[str] = None) -> Dict:
251
- """Execute with Temporal for durable workflow orchestration."""
252
- # Use passed workflow_id (from deployment) or generate new one
253
- if not workflow_id:
254
- workflow_id = f"temporal_{session_id}_{int(time.time() * 1000)}"
255
-
256
- logger.info(
257
- "Executing workflow via Temporal",
258
- workflow_id=workflow_id,
259
- node_count=len(nodes),
260
- )
261
-
262
- result = await self._temporal_executor.execute_workflow(
263
- workflow_id=workflow_id,
264
- nodes=nodes,
265
- edges=edges,
266
- session_id=session_id,
267
- enable_caching=True,
268
- )
269
-
270
- # Notify status callback for completed nodes if provided
271
- if status_callback and result.get("success"):
272
- for node_id in result.get("nodes_executed", []):
273
- try:
274
- await status_callback(
275
- node_id,
276
- "completed",
277
- result.get("outputs", {}).get(node_id, {}),
278
- )
279
- except Exception:
280
- pass
281
-
282
- return {
283
- "success": result.get("success", False),
284
- "execution_id": result.get("execution_id"),
285
- "nodes_executed": result.get("nodes_executed", []),
286
- "outputs": result.get("outputs", {}),
287
- "errors": result.get("errors", []),
288
- "execution_time": result.get("execution_time", time.time() - start_time),
289
- "temporal_execution": True,
290
- "timestamp": datetime.now().isoformat(),
291
- }
292
-
293
- async def _execute_parallel(self, nodes, edges, session_id, status_callback, start_time, workflow_id: Optional[str] = None) -> Dict:
294
- """Execute with parallel orchestration engine."""
295
- # Use passed workflow_id (from deployment) or generate new one
296
- if not workflow_id:
297
- workflow_id = f"workflow_{session_id}_{int(time.time() * 1000)}"
298
- executor = self._get_workflow_executor(status_callback)
299
-
300
- result = await executor.execute_workflow(
301
- workflow_id=workflow_id,
302
- nodes=nodes,
303
- edges=edges,
304
- session_id=session_id,
305
- enable_caching=True,
306
- )
307
-
308
- return {
309
- "success": result.get("success", False),
310
- "execution_id": result.get("execution_id"),
311
- "nodes_executed": result.get("nodes_executed", []),
312
- "outputs": result.get("outputs", {}),
313
- "errors": result.get("errors", []),
314
- "execution_time": result.get("execution_time", time.time() - start_time),
315
- "parallel_execution": True,
316
- "timestamp": datetime.now().isoformat(),
317
- }
318
-
319
- async def _execute_sequential(self, nodes, edges, session_id, status_callback, start_time, workflow_id: Optional[str] = None) -> Dict:
320
- """Execute nodes sequentially (fallback mode)."""
321
- start_node = self._find_start_node(nodes)
322
- execution_order = self._build_execution_order(start_node, nodes, edges)
323
-
324
- results = {}
325
- executed = []
326
-
327
- for node in execution_order:
328
- node_id = node['id']
329
- node_type = node.get('type', 'unknown')
330
-
331
- # Skip pre-executed trigger nodes
332
- if node.get('_pre_executed'):
333
- executed.append(node_id)
334
- continue
335
-
336
- # Skip disabled nodes (n8n-style disable)
337
- if node.get('data', {}).get('disabled'):
338
- logger.debug(f"Skipping disabled node: {node_id}")
339
- executed.append(node_id)
340
- if status_callback:
341
- try:
342
- await status_callback(node_id, "skipped", {"disabled": True})
343
- except Exception:
344
- pass
345
- continue
346
-
347
- # Notify executing
348
- if status_callback:
349
- try:
350
- await status_callback(node_id, "executing", {})
351
- except Exception:
352
- pass
353
-
354
- # Execute with workflow_id for per-workflow status scoping (n8n pattern)
355
- result = await self.execute_node(
356
- node_id=node_id,
357
- node_type=node_type,
358
- parameters={},
359
- nodes=nodes,
360
- edges=edges,
361
- session_id=session_id,
362
- workflow_id=workflow_id,
363
- )
364
-
365
- results[node_id] = result
366
- executed.append(node_id)
367
-
368
- # Notify completed
369
- if status_callback:
370
- status = "completed" if result.get("success") else "error"
371
- try:
372
- await status_callback(node_id, status, result)
373
- except Exception:
374
- pass
375
-
376
- if not result.get("success") and self._settings.get("stop_on_error"):
377
- break
378
-
379
- return {
380
- "success": all(r.get("success", False) for r in results.values()),
381
- "nodes_executed": executed,
382
- "node_results": results,
383
- "execution_time": time.time() - start_time,
384
- "parallel_execution": False,
385
- "timestamp": datetime.now().isoformat(),
386
- }
387
-
388
- # =========================================================================
389
- # DEPLOYMENT
390
- # =========================================================================
391
-
392
- async def deploy_workflow(
393
- self,
394
- nodes: List[Dict],
395
- edges: List[Dict],
396
- session_id: str = "default",
397
- status_callback=None,
398
- workflow_id: Optional[str] = None,
399
- ) -> Dict[str, Any]:
400
- """Deploy workflow in event-driven mode.
401
-
402
- Args:
403
- nodes: Workflow nodes
404
- edges: Workflow edges
405
- session_id: Session identifier
406
- status_callback: Status update callback
407
- workflow_id: Workflow ID for per-workflow deployment tracking
408
- """
409
- manager = self._get_deployment_manager()
410
- return await manager.deploy(nodes, edges, session_id, status_callback, workflow_id)
411
-
412
- async def cancel_deployment(self, workflow_id: Optional[str] = None) -> Dict[str, Any]:
413
- """Cancel active deployment.
414
-
415
- Args:
416
- workflow_id: Specific workflow to cancel. If None, cancels first running deployment.
417
- """
418
- manager = self._get_deployment_manager()
419
- return await manager.cancel(workflow_id)
420
-
421
- def get_deployment_status(self, workflow_id: Optional[str] = None) -> Dict[str, Any]:
422
- """Get deployment status.
423
-
424
- Args:
425
- workflow_id: Get status for specific workflow. If None, returns global status.
426
- """
427
- manager = self._get_deployment_manager()
428
- return manager.get_status(workflow_id)
429
-
430
- def is_deployment_running(self, workflow_id: Optional[str] = None) -> bool:
431
- """Check if deployment is running.
432
-
433
- Args:
434
- workflow_id: Check specific workflow. If None, checks if ANY deployment is running.
435
- """
436
- manager = self._get_deployment_manager()
437
- if workflow_id:
438
- return manager.is_workflow_deployed(workflow_id)
439
- return manager.is_running
440
-
441
- def is_workflow_deployed(self, workflow_id: str) -> bool:
442
- """Check if a specific workflow is deployed."""
443
- return self._get_deployment_manager().is_workflow_deployed(workflow_id)
444
-
445
- def get_deployed_workflows(self) -> List[str]:
446
- """Get list of deployed workflow IDs."""
447
- return self._get_deployment_manager().get_deployed_workflows()
448
-
449
- # =========================================================================
450
- # OUTPUT STORAGE
451
- # =========================================================================
452
-
453
- async def store_node_output(
454
- self,
455
- session_id: str,
456
- node_id: str,
457
- output_name: str,
458
- data: Dict[str, Any],
459
- ) -> None:
460
- """Store node execution output."""
461
- key = f"{session_id}_{node_id}"
462
- if key not in self._outputs:
463
- self._outputs[key] = {}
464
- self._outputs[key][output_name] = data
465
- await self.database.save_node_output(node_id, session_id, output_name, data)
466
-
467
- async def get_node_output(
468
- self,
469
- session_id: str,
470
- node_id: str,
471
- output_name: str,
472
- ) -> Optional[Dict[str, Any]]:
473
- """Get stored node output."""
474
- key = f"{session_id}_{node_id}"
475
- output = self._outputs.get(key, {}).get(output_name)
476
-
477
- if output is None:
478
- output = await self.database.get_node_output(node_id, session_id, output_name)
479
- if output:
480
- if key not in self._outputs:
481
- self._outputs[key] = {}
482
- self._outputs[key][output_name] = output
483
-
484
- # Special handling for start nodes
485
- if output is None and node_id.startswith('start-'):
486
- import json
487
- params = await self.database.get_node_parameters(node_id)
488
- if params and 'initialData' in params:
489
- try:
490
- output = json.loads(params.get('initialData', '{}'))
491
- except Exception:
492
- output = {}
493
-
494
- return output
495
-
496
- async def get_workflow_node_output(
497
- self,
498
- node_id: str,
499
- output_name: str = "output_0",
500
- session_id: str = "default",
501
- ) -> Dict[str, Any]:
502
- """Get stored output data for a node."""
503
- output = await self.get_node_output(session_id, node_id, output_name)
504
- if output:
505
- return {"success": True, "node_id": node_id, "data": output}
506
- return {"success": False, "node_id": node_id, "error": "No output found"}
507
-
508
- async def clear_all_outputs(self, session_id: str = "default") -> None:
509
- """Clear all outputs for a session."""
510
- keys = [k for k in self._outputs if k.startswith(f"{session_id}_")]
511
- for k in keys:
512
- del self._outputs[k]
513
- await self.database.clear_session_outputs(session_id)
514
-
515
- # =========================================================================
516
- # HELPERS
517
- # =========================================================================
518
-
519
- def _find_start_node(self, nodes: List[Dict]) -> Optional[Dict]:
520
- """Find workflow entry point."""
521
- # Priority: start > cronScheduler > other triggers
522
- for node in nodes:
523
- if node.get('type') == 'start':
524
- return node
525
- for node in nodes:
526
- if node.get('type') == 'cronScheduler':
527
- return node
528
- for node in nodes:
529
- if node.get('type') in WORKFLOW_TRIGGER_TYPES:
530
- return node
531
- return None
532
-
533
- def _build_execution_order(self, start: Dict, nodes: List[Dict], edges: List[Dict]) -> List[Dict]:
534
- """Build BFS execution order from start node."""
535
- visited = set()
536
- order = []
537
- queue = [start['id']]
538
-
539
- # Build adjacency map
540
- adj = {}
541
- for e in edges:
542
- src = e.get('source')
543
- if src:
544
- adj.setdefault(src, []).append(e.get('target'))
545
-
546
- node_map = {n['id']: n for n in nodes}
547
-
548
- while queue:
549
- nid = queue.pop(0)
550
- if nid in visited:
551
- continue
552
- visited.add(nid)
553
- node = node_map.get(nid)
554
- if node:
555
- order.append(node)
556
- queue.extend(t for t in adj.get(nid, []) if t not in visited)
557
-
558
- return order
559
-
560
- def _error_result(self, error: str, start_time: float) -> Dict:
561
- """Build error result."""
562
- return {
563
- "success": False,
564
- "error": error,
565
- "nodes_executed": [],
566
- "execution_time": time.time() - start_time,
567
- "timestamp": datetime.now().isoformat(),
568
- }
569
-
570
- # =========================================================================
571
- # SETTINGS
572
- # =========================================================================
573
-
574
- async def load_deployment_settings(self) -> Dict[str, Any]:
575
- """Load deployment settings from database."""
576
- try:
577
- db = await self.database.get_deployment_settings()
578
- if db:
579
- self._settings.update(db)
580
- except Exception:
581
- pass
582
- return self._settings.copy()
583
-
584
- async def update_deployment_settings(self, settings: Dict[str, Any]) -> Dict[str, Any]:
585
- """Update deployment settings."""
586
- self._settings.update(settings)
587
- await self.database.save_deployment_settings(self._settings)
588
- return self._settings.copy()
589
-
590
- def get_deployment_settings(self) -> Dict[str, Any]:
591
- """Get current deployment settings."""
592
- return self._settings.copy()
593
-
594
- @property
595
- def node_outputs(self) -> Dict[str, Dict[str, Any]]:
596
- """Backward compatibility: expose outputs as node_outputs."""
597
- return self._outputs
1
+ """Workflow Service - Facade for workflow execution and deployment.
2
+
3
+ This is a thin facade that delegates to specialized modules:
4
+ - NodeExecutor: Single node execution
5
+ - ParameterResolver: Template variable resolution
6
+ - DeploymentManager: Event-driven deployment lifecycle
7
+ - WorkflowExecutor: Parallel/sequential orchestration
8
+ - TemporalExecutor: Durable workflow execution (optional)
9
+
10
+ Following n8n/Conductor patterns for clean separation of concerns.
11
+ """
12
+
13
+ import time
14
+ from datetime import datetime
15
+ from typing import Dict, Any, List, Optional, TYPE_CHECKING
16
+
17
+ from core.logging import get_logger
18
+ from constants import WORKFLOW_TRIGGER_TYPES
19
+ from services.node_executor import NodeExecutor
20
+ from services.parameter_resolver import ParameterResolver
21
+ from services.deployment import DeploymentManager
22
+ from services.execution import WorkflowExecutor, ExecutionCache
23
+
24
+ if TYPE_CHECKING:
25
+ from core.config import Settings
26
+ from core.database import Database
27
+ from core.cache import CacheService
28
+ from services.ai import AIService
29
+ from services.maps import MapsService
30
+ from services.text import TextService
31
+ from services.android_service import AndroidService
32
+ from services.temporal import TemporalExecutor
33
+
34
+ logger = get_logger(__name__)
35
+
36
+
37
+ class WorkflowService:
38
+ """Workflow execution and deployment service.
39
+
40
+ Thin facade delegating to specialized modules for:
41
+ - Node execution (NodeExecutor)
42
+ - Parameter resolution (ParameterResolver)
43
+ - Deployment lifecycle (DeploymentManager)
44
+ - Workflow orchestration (WorkflowExecutor or TemporalExecutor)
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ database: "Database",
50
+ ai_service: "AIService",
51
+ maps_service: "MapsService",
52
+ text_service: "TextService",
53
+ android_service: "AndroidService",
54
+ cache: "CacheService",
55
+ settings: "Settings",
56
+ ):
57
+ self.database = database
58
+ self.settings = settings
59
+
60
+ # In-memory output storage (fast access during execution)
61
+ self._outputs: Dict[str, Dict[str, Any]] = {}
62
+
63
+ # Initialize NodeExecutor
64
+ self._node_executor = NodeExecutor(
65
+ database=database,
66
+ ai_service=ai_service,
67
+ maps_service=maps_service,
68
+ text_service=text_service,
69
+ android_service=android_service,
70
+ settings=settings,
71
+ output_store=self.store_node_output,
72
+ )
73
+
74
+ # Initialize ParameterResolver
75
+ self._param_resolver = ParameterResolver(
76
+ database=database,
77
+ get_output_fn=self.get_node_output,
78
+ )
79
+
80
+ # Initialize Execution Cache
81
+ self._execution_cache = ExecutionCache(cache)
82
+ self._workflow_executor: Optional[WorkflowExecutor] = None
83
+
84
+ # Temporal executor (set via set_temporal_executor when enabled)
85
+ self._temporal_executor: Optional["TemporalExecutor"] = None
86
+
87
+ # Initialize DeploymentManager (lazy - needs broadcaster)
88
+ self._deployment_manager: Optional[DeploymentManager] = None
89
+ self._broadcaster = None
90
+
91
+ # Deployment settings
92
+ self._settings = {
93
+ "stop_on_error": False,
94
+ "max_concurrent_runs": 100,
95
+ "use_parallel_executor": True,
96
+ }
97
+
98
+ def set_temporal_executor(self, executor: "TemporalExecutor") -> None:
99
+ """Set the Temporal executor for durable workflow execution.
100
+
101
+ Args:
102
+ executor: Configured TemporalExecutor instance
103
+ """
104
+ self._temporal_executor = executor
105
+ logger.info("Temporal executor configured for workflow execution")
106
+
107
+ def _get_deployment_manager(self) -> DeploymentManager:
108
+ """Get or create DeploymentManager."""
109
+ if self._deployment_manager is None:
110
+ from services.status_broadcaster import get_status_broadcaster
111
+ self._broadcaster = get_status_broadcaster()
112
+ self._deployment_manager = DeploymentManager(
113
+ database=self.database,
114
+ execute_workflow_fn=self.execute_workflow,
115
+ store_output_fn=self.store_node_output,
116
+ broadcaster=self._broadcaster,
117
+ )
118
+ return self._deployment_manager
119
+
120
+ def _get_workflow_executor(self, status_callback=None) -> WorkflowExecutor:
121
+ """Get or create WorkflowExecutor."""
122
+ if self._workflow_executor is None or status_callback:
123
+ self._workflow_executor = WorkflowExecutor(
124
+ cache=self._execution_cache,
125
+ node_executor=self._execute_node_adapter,
126
+ status_callback=status_callback,
127
+ dlq_enabled=self.settings.dlq_enabled,
128
+ )
129
+ return self._workflow_executor
130
+
131
+ # =========================================================================
132
+ # NODE EXECUTION
133
+ # =========================================================================
134
+
135
+ async def execute_node(
136
+ self,
137
+ node_id: str,
138
+ node_type: str,
139
+ parameters: Dict[str, Any],
140
+ nodes: List[Dict] = None,
141
+ edges: List[Dict] = None,
142
+ session_id: str = "default",
143
+ execution_id: str = None,
144
+ workflow_id: str = None,
145
+ ) -> Dict[str, Any]:
146
+ """Execute a single workflow node."""
147
+ context = {
148
+ "nodes": nodes,
149
+ "edges": edges,
150
+ "session_id": session_id,
151
+ "execution_id": execution_id,
152
+ "workflow_id": workflow_id, # For per-workflow status scoping (n8n pattern)
153
+ "get_output_fn": self.get_node_output,
154
+ }
155
+ return await self._node_executor.execute(
156
+ node_id=node_id,
157
+ node_type=node_type,
158
+ parameters=parameters,
159
+ context=context,
160
+ resolve_params_fn=self._param_resolver.resolve,
161
+ )
162
+
163
+ async def _execute_node_adapter(
164
+ self,
165
+ node_id: str,
166
+ node_type: str,
167
+ parameters: Dict[str, Any],
168
+ context: Dict[str, Any],
169
+ ) -> Dict[str, Any]:
170
+ """Adapter for WorkflowExecutor to call NodeExecutor."""
171
+ return await self.execute_node(
172
+ node_id=node_id,
173
+ node_type=node_type,
174
+ parameters=parameters,
175
+ nodes=context.get("nodes"),
176
+ edges=context.get("edges"),
177
+ session_id=context.get("session_id", "default"),
178
+ execution_id=context.get("execution_id"),
179
+ workflow_id=context.get("workflow_id"), # Pass workflow_id for status scoping
180
+ )
181
+
182
+ # =========================================================================
183
+ # WORKFLOW EXECUTION
184
+ # =========================================================================
185
+
186
+ async def execute_workflow(
187
+ self,
188
+ nodes: List[Dict],
189
+ edges: List[Dict],
190
+ session_id: str = "default",
191
+ status_callback=None,
192
+ use_parallel: bool = None,
193
+ skip_clear_outputs: bool = False,
194
+ workflow_id: Optional[str] = None,
195
+ use_temporal: bool = None,
196
+ ) -> Dict[str, Any]:
197
+ """Execute entire workflow.
198
+
199
+ Args:
200
+ nodes: Workflow nodes
201
+ edges: Workflow edges
202
+ session_id: Session identifier
203
+ status_callback: Status update callback
204
+ use_parallel: Force parallel/sequential execution
205
+ skip_clear_outputs: Skip clearing outputs (for deployment runs)
206
+ workflow_id: Workflow ID for per-workflow status scoping (n8n pattern)
207
+ use_temporal: Force Temporal execution (None = use settings default)
208
+ """
209
+ start_time = time.time()
210
+
211
+ # Clear outputs unless skipped
212
+ if not skip_clear_outputs:
213
+ await self.clear_all_outputs(session_id)
214
+
215
+ if not nodes:
216
+ return self._error_result("No nodes in workflow", start_time)
217
+
218
+ # Find start node
219
+ start_node = self._find_start_node(nodes)
220
+ if not start_node:
221
+ return self._error_result("No start node found", start_time)
222
+
223
+ # Determine execution mode
224
+ if use_parallel is None:
225
+ use_parallel = self._settings.get("use_parallel_executor", True)
226
+
227
+ # Check if Temporal execution is requested
228
+ if use_temporal is None:
229
+ use_temporal = self.settings.temporal_enabled
230
+
231
+ # Use Temporal if enabled and executor is configured
232
+ if use_temporal and self._temporal_executor is not None:
233
+ return await self._execute_temporal(nodes, edges, session_id, status_callback, start_time, workflow_id)
234
+
235
+ # Log warning if Temporal was requested but not available
236
+ if use_temporal and self._temporal_executor is None:
237
+ logger.warning(
238
+ "Temporal execution requested but executor not configured. "
239
+ "Falling back to parallel/sequential execution. "
240
+ "Check TEMPORAL_ENABLED and Temporal server connection."
241
+ )
242
+
243
+ # Use parallel executor if enabled and Redis available
244
+ if use_parallel and self.settings.redis_enabled:
245
+ return await self._execute_parallel(nodes, edges, session_id, status_callback, start_time, workflow_id)
246
+
247
+ # Fall back to sequential
248
+ return await self._execute_sequential(nodes, edges, session_id, status_callback, start_time, workflow_id)
249
+
250
+ async def _execute_temporal(self, nodes, edges, session_id, status_callback, start_time, workflow_id: Optional[str] = None) -> Dict:
251
+ """Execute with Temporal for durable workflow orchestration."""
252
+ # Use passed workflow_id (from deployment) or generate new one
253
+ if not workflow_id:
254
+ workflow_id = f"temporal_{session_id}_{int(time.time() * 1000)}"
255
+
256
+ logger.info(
257
+ "Executing workflow via Temporal",
258
+ workflow_id=workflow_id,
259
+ node_count=len(nodes),
260
+ )
261
+
262
+ result = await self._temporal_executor.execute_workflow(
263
+ workflow_id=workflow_id,
264
+ nodes=nodes,
265
+ edges=edges,
266
+ session_id=session_id,
267
+ enable_caching=True,
268
+ )
269
+
270
+ # Notify status callback for completed nodes if provided
271
+ if status_callback and result.get("success"):
272
+ for node_id in result.get("nodes_executed", []):
273
+ try:
274
+ await status_callback(
275
+ node_id,
276
+ "completed",
277
+ result.get("outputs", {}).get(node_id, {}),
278
+ )
279
+ except Exception:
280
+ pass
281
+
282
+ return {
283
+ "success": result.get("success", False),
284
+ "execution_id": result.get("execution_id"),
285
+ "nodes_executed": result.get("nodes_executed", []),
286
+ "outputs": result.get("outputs", {}),
287
+ "errors": result.get("errors", []),
288
+ "execution_time": result.get("execution_time", time.time() - start_time),
289
+ "temporal_execution": True,
290
+ "timestamp": datetime.now().isoformat(),
291
+ }
292
+
293
+ async def _execute_parallel(self, nodes, edges, session_id, status_callback, start_time, workflow_id: Optional[str] = None) -> Dict:
294
+ """Execute with parallel orchestration engine."""
295
+ # Use passed workflow_id (from deployment) or generate new one
296
+ if not workflow_id:
297
+ workflow_id = f"workflow_{session_id}_{int(time.time() * 1000)}"
298
+ executor = self._get_workflow_executor(status_callback)
299
+
300
+ result = await executor.execute_workflow(
301
+ workflow_id=workflow_id,
302
+ nodes=nodes,
303
+ edges=edges,
304
+ session_id=session_id,
305
+ enable_caching=True,
306
+ )
307
+
308
+ return {
309
+ "success": result.get("success", False),
310
+ "execution_id": result.get("execution_id"),
311
+ "nodes_executed": result.get("nodes_executed", []),
312
+ "outputs": result.get("outputs", {}),
313
+ "errors": result.get("errors", []),
314
+ "execution_time": result.get("execution_time", time.time() - start_time),
315
+ "parallel_execution": True,
316
+ "timestamp": datetime.now().isoformat(),
317
+ }
318
+
319
+ async def _execute_sequential(self, nodes, edges, session_id, status_callback, start_time, workflow_id: Optional[str] = None) -> Dict:
320
+ """Execute nodes sequentially (fallback mode)."""
321
+ start_node = self._find_start_node(nodes)
322
+ execution_order = self._build_execution_order(start_node, nodes, edges)
323
+
324
+ results = {}
325
+ executed = []
326
+
327
+ for node in execution_order:
328
+ node_id = node['id']
329
+ node_type = node.get('type', 'unknown')
330
+
331
+ # Skip pre-executed trigger nodes
332
+ if node.get('_pre_executed'):
333
+ executed.append(node_id)
334
+ continue
335
+
336
+ # Skip disabled nodes (n8n-style disable)
337
+ if node.get('data', {}).get('disabled'):
338
+ logger.debug(f"Skipping disabled node: {node_id}")
339
+ executed.append(node_id)
340
+ if status_callback:
341
+ try:
342
+ await status_callback(node_id, "skipped", {"disabled": True})
343
+ except Exception:
344
+ pass
345
+ continue
346
+
347
+ # Notify executing
348
+ if status_callback:
349
+ try:
350
+ await status_callback(node_id, "executing", {})
351
+ except Exception:
352
+ pass
353
+
354
+ # Execute with workflow_id for per-workflow status scoping (n8n pattern)
355
+ result = await self.execute_node(
356
+ node_id=node_id,
357
+ node_type=node_type,
358
+ parameters={},
359
+ nodes=nodes,
360
+ edges=edges,
361
+ session_id=session_id,
362
+ workflow_id=workflow_id,
363
+ )
364
+
365
+ results[node_id] = result
366
+ executed.append(node_id)
367
+
368
+ # Notify completed
369
+ if status_callback:
370
+ status = "completed" if result.get("success") else "error"
371
+ try:
372
+ await status_callback(node_id, status, result)
373
+ except Exception:
374
+ pass
375
+
376
+ if not result.get("success") and self._settings.get("stop_on_error"):
377
+ break
378
+
379
+ return {
380
+ "success": all(r.get("success", False) for r in results.values()),
381
+ "nodes_executed": executed,
382
+ "node_results": results,
383
+ "execution_time": time.time() - start_time,
384
+ "parallel_execution": False,
385
+ "timestamp": datetime.now().isoformat(),
386
+ }
387
+
388
+ # =========================================================================
389
+ # DEPLOYMENT
390
+ # =========================================================================
391
+
392
+ async def deploy_workflow(
393
+ self,
394
+ nodes: List[Dict],
395
+ edges: List[Dict],
396
+ session_id: str = "default",
397
+ status_callback=None,
398
+ workflow_id: Optional[str] = None,
399
+ ) -> Dict[str, Any]:
400
+ """Deploy workflow in event-driven mode.
401
+
402
+ Args:
403
+ nodes: Workflow nodes
404
+ edges: Workflow edges
405
+ session_id: Session identifier
406
+ status_callback: Status update callback
407
+ workflow_id: Workflow ID for per-workflow deployment tracking
408
+ """
409
+ manager = self._get_deployment_manager()
410
+ return await manager.deploy(nodes, edges, session_id, status_callback, workflow_id)
411
+
412
+ async def cancel_deployment(self, workflow_id: Optional[str] = None) -> Dict[str, Any]:
413
+ """Cancel active deployment.
414
+
415
+ Args:
416
+ workflow_id: Specific workflow to cancel. If None, cancels first running deployment.
417
+ """
418
+ manager = self._get_deployment_manager()
419
+ return await manager.cancel(workflow_id)
420
+
421
+ def get_deployment_status(self, workflow_id: Optional[str] = None) -> Dict[str, Any]:
422
+ """Get deployment status.
423
+
424
+ Args:
425
+ workflow_id: Get status for specific workflow. If None, returns global status.
426
+ """
427
+ manager = self._get_deployment_manager()
428
+ return manager.get_status(workflow_id)
429
+
430
+ def is_deployment_running(self, workflow_id: Optional[str] = None) -> bool:
431
+ """Check if deployment is running.
432
+
433
+ Args:
434
+ workflow_id: Check specific workflow. If None, checks if ANY deployment is running.
435
+ """
436
+ manager = self._get_deployment_manager()
437
+ if workflow_id:
438
+ return manager.is_workflow_deployed(workflow_id)
439
+ return manager.is_running
440
+
441
+ def is_workflow_deployed(self, workflow_id: str) -> bool:
442
+ """Check if a specific workflow is deployed."""
443
+ return self._get_deployment_manager().is_workflow_deployed(workflow_id)
444
+
445
+ def get_deployed_workflows(self) -> List[str]:
446
+ """Get list of deployed workflow IDs."""
447
+ return self._get_deployment_manager().get_deployed_workflows()
448
+
449
+ # =========================================================================
450
+ # OUTPUT STORAGE
451
+ # =========================================================================
452
+
453
+ async def store_node_output(
454
+ self,
455
+ session_id: str,
456
+ node_id: str,
457
+ output_name: str,
458
+ data: Dict[str, Any],
459
+ ) -> None:
460
+ """Store node execution output."""
461
+ key = f"{session_id}_{node_id}"
462
+ if key not in self._outputs:
463
+ self._outputs[key] = {}
464
+ self._outputs[key][output_name] = data
465
+ await self.database.save_node_output(node_id, session_id, output_name, data)
466
+
467
+ async def get_node_output(
468
+ self,
469
+ session_id: str,
470
+ node_id: str,
471
+ output_name: str,
472
+ ) -> Optional[Dict[str, Any]]:
473
+ """Get stored node output."""
474
+ key = f"{session_id}_{node_id}"
475
+ output = self._outputs.get(key, {}).get(output_name)
476
+
477
+ if output is None:
478
+ output = await self.database.get_node_output(node_id, session_id, output_name)
479
+ if output:
480
+ if key not in self._outputs:
481
+ self._outputs[key] = {}
482
+ self._outputs[key][output_name] = output
483
+
484
+ # Special handling for start nodes
485
+ if output is None and node_id.startswith('start-'):
486
+ import json
487
+ params = await self.database.get_node_parameters(node_id)
488
+ if params and 'initialData' in params:
489
+ try:
490
+ output = json.loads(params.get('initialData', '{}'))
491
+ except Exception:
492
+ output = {}
493
+
494
+ return output
495
+
496
+ async def get_workflow_node_output(
497
+ self,
498
+ node_id: str,
499
+ output_name: str = "output_0",
500
+ session_id: str = "default",
501
+ ) -> Dict[str, Any]:
502
+ """Get stored output data for a node."""
503
+ output = await self.get_node_output(session_id, node_id, output_name)
504
+ if output:
505
+ return {"success": True, "node_id": node_id, "data": output}
506
+ return {"success": False, "node_id": node_id, "error": "No output found"}
507
+
508
+ async def clear_all_outputs(self, session_id: str = "default") -> None:
509
+ """Clear all outputs for a session."""
510
+ keys = [k for k in self._outputs if k.startswith(f"{session_id}_")]
511
+ for k in keys:
512
+ del self._outputs[k]
513
+ await self.database.clear_session_outputs(session_id)
514
+
515
+ # =========================================================================
516
+ # HELPERS
517
+ # =========================================================================
518
+
519
+ def _find_start_node(self, nodes: List[Dict]) -> Optional[Dict]:
520
+ """Find workflow entry point."""
521
+ # Priority: start > cronScheduler > other triggers
522
+ for node in nodes:
523
+ if node.get('type') == 'start':
524
+ return node
525
+ for node in nodes:
526
+ if node.get('type') == 'cronScheduler':
527
+ return node
528
+ for node in nodes:
529
+ if node.get('type') in WORKFLOW_TRIGGER_TYPES:
530
+ return node
531
+ return None
532
+
533
+ def _build_execution_order(self, start: Dict, nodes: List[Dict], edges: List[Dict]) -> List[Dict]:
534
+ """Build BFS execution order from start node."""
535
+ visited = set()
536
+ order = []
537
+ queue = [start['id']]
538
+
539
+ # Build adjacency map
540
+ adj = {}
541
+ for e in edges:
542
+ src = e.get('source')
543
+ if src:
544
+ adj.setdefault(src, []).append(e.get('target'))
545
+
546
+ node_map = {n['id']: n for n in nodes}
547
+
548
+ while queue:
549
+ nid = queue.pop(0)
550
+ if nid in visited:
551
+ continue
552
+ visited.add(nid)
553
+ node = node_map.get(nid)
554
+ if node:
555
+ order.append(node)
556
+ queue.extend(t for t in adj.get(nid, []) if t not in visited)
557
+
558
+ return order
559
+
560
+ def _error_result(self, error: str, start_time: float) -> Dict:
561
+ """Build error result."""
562
+ return {
563
+ "success": False,
564
+ "error": error,
565
+ "nodes_executed": [],
566
+ "execution_time": time.time() - start_time,
567
+ "timestamp": datetime.now().isoformat(),
568
+ }
569
+
570
+ # =========================================================================
571
+ # SETTINGS
572
+ # =========================================================================
573
+
574
+ async def load_deployment_settings(self) -> Dict[str, Any]:
575
+ """Load deployment settings from database."""
576
+ try:
577
+ db = await self.database.get_deployment_settings()
578
+ if db:
579
+ self._settings.update(db)
580
+ except Exception:
581
+ pass
582
+ return self._settings.copy()
583
+
584
+ async def update_deployment_settings(self, settings: Dict[str, Any]) -> Dict[str, Any]:
585
+ """Update deployment settings."""
586
+ self._settings.update(settings)
587
+ await self.database.save_deployment_settings(self._settings)
588
+ return self._settings.copy()
589
+
590
+ def get_deployment_settings(self) -> Dict[str, Any]:
591
+ """Get current deployment settings."""
592
+ return self._settings.copy()
593
+
594
+ @property
595
+ def node_outputs(self) -> Dict[str, Dict[str, Any]]:
596
+ """Backward compatibility: expose outputs as node_outputs."""
597
+ return self._outputs