machinaos 0.0.1

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 (288) hide show
  1. package/.env.template +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/bin/cli.js +159 -0
  5. package/client/.dockerignore +45 -0
  6. package/client/Dockerfile +68 -0
  7. package/client/eslint.config.js +29 -0
  8. package/client/index.html +13 -0
  9. package/client/nginx.conf +66 -0
  10. package/client/package.json +48 -0
  11. package/client/src/App.tsx +27 -0
  12. package/client/src/Dashboard.tsx +1173 -0
  13. package/client/src/ParameterPanel.tsx +301 -0
  14. package/client/src/components/AIAgentNode.tsx +321 -0
  15. package/client/src/components/APIKeyValidator.tsx +118 -0
  16. package/client/src/components/ClaudeChatModelNode.tsx +18 -0
  17. package/client/src/components/ConditionalEdge.tsx +189 -0
  18. package/client/src/components/CredentialsModal.tsx +306 -0
  19. package/client/src/components/EdgeConditionEditor.tsx +443 -0
  20. package/client/src/components/GeminiChatModelNode.tsx +18 -0
  21. package/client/src/components/GenericNode.tsx +357 -0
  22. package/client/src/components/LocationParameterPanel.tsx +154 -0
  23. package/client/src/components/ModelNode.tsx +286 -0
  24. package/client/src/components/OpenAIChatModelNode.tsx +18 -0
  25. package/client/src/components/OutputPanel.tsx +471 -0
  26. package/client/src/components/ParameterRenderer.tsx +1874 -0
  27. package/client/src/components/SkillEditorModal.tsx +417 -0
  28. package/client/src/components/SquareNode.tsx +797 -0
  29. package/client/src/components/StartNode.tsx +250 -0
  30. package/client/src/components/ToolkitNode.tsx +365 -0
  31. package/client/src/components/TriggerNode.tsx +463 -0
  32. package/client/src/components/auth/LoginPage.tsx +247 -0
  33. package/client/src/components/auth/ProtectedRoute.tsx +59 -0
  34. package/client/src/components/base/BaseChatModelNode.tsx +271 -0
  35. package/client/src/components/icons/AIProviderIcons.tsx +50 -0
  36. package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
  37. package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
  38. package/client/src/components/maps/index.ts +26 -0
  39. package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
  40. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
  41. package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
  42. package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
  43. package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
  44. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
  45. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
  46. package/client/src/components/parameterPanel/index.ts +42 -0
  47. package/client/src/components/shared/DataPanel.tsx +142 -0
  48. package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
  49. package/client/src/components/ui/AIResultModal.tsx +204 -0
  50. package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
  51. package/client/src/components/ui/CodeEditor.tsx +81 -0
  52. package/client/src/components/ui/CollapsibleSection.tsx +88 -0
  53. package/client/src/components/ui/ComponentItem.tsx +154 -0
  54. package/client/src/components/ui/ComponentPalette.tsx +321 -0
  55. package/client/src/components/ui/ConsolePanel.tsx +1074 -0
  56. package/client/src/components/ui/ErrorBoundary.tsx +196 -0
  57. package/client/src/components/ui/InputNodesPanel.tsx +204 -0
  58. package/client/src/components/ui/MapSelector.tsx +314 -0
  59. package/client/src/components/ui/Modal.tsx +149 -0
  60. package/client/src/components/ui/NodeContextMenu.tsx +192 -0
  61. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
  62. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
  63. package/client/src/components/ui/SettingsPanel.tsx +243 -0
  64. package/client/src/components/ui/TopToolbar.tsx +736 -0
  65. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
  66. package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
  67. package/client/src/config/antdTheme.ts +186 -0
  68. package/client/src/config/api.ts +54 -0
  69. package/client/src/contexts/AuthContext.tsx +221 -0
  70. package/client/src/contexts/ThemeContext.tsx +42 -0
  71. package/client/src/contexts/WebSocketContext.tsx +1971 -0
  72. package/client/src/factories/baseChatModelFactory.ts +256 -0
  73. package/client/src/hooks/useAndroidOperations.ts +164 -0
  74. package/client/src/hooks/useApiKeyValidation.ts +107 -0
  75. package/client/src/hooks/useApiKeys.ts +238 -0
  76. package/client/src/hooks/useAppTheme.ts +17 -0
  77. package/client/src/hooks/useComponentPalette.ts +51 -0
  78. package/client/src/hooks/useCopyPaste.ts +155 -0
  79. package/client/src/hooks/useDragAndDrop.ts +124 -0
  80. package/client/src/hooks/useDragVariable.ts +88 -0
  81. package/client/src/hooks/useExecution.ts +313 -0
  82. package/client/src/hooks/useParameterPanel.ts +176 -0
  83. package/client/src/hooks/useReactFlowNodes.ts +189 -0
  84. package/client/src/hooks/useToolSchema.ts +209 -0
  85. package/client/src/hooks/useWhatsApp.ts +196 -0
  86. package/client/src/hooks/useWorkflowManagement.ts +46 -0
  87. package/client/src/index.css +315 -0
  88. package/client/src/main.tsx +19 -0
  89. package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
  90. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
  91. package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
  92. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
  93. package/client/src/nodeDefinitions/chatNodes.ts +135 -0
  94. package/client/src/nodeDefinitions/codeNodes.ts +54 -0
  95. package/client/src/nodeDefinitions/documentNodes.ts +379 -0
  96. package/client/src/nodeDefinitions/index.ts +15 -0
  97. package/client/src/nodeDefinitions/locationNodes.ts +463 -0
  98. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
  99. package/client/src/nodeDefinitions/skillNodes.ts +211 -0
  100. package/client/src/nodeDefinitions/toolNodes.ts +198 -0
  101. package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
  102. package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
  103. package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
  104. package/client/src/nodeDefinitions.ts +104 -0
  105. package/client/src/schemas/workflowSchema.ts +264 -0
  106. package/client/src/services/dynamicParameterService.ts +96 -0
  107. package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
  108. package/client/src/services/executionService.ts +232 -0
  109. package/client/src/services/workflowApi.ts +91 -0
  110. package/client/src/store/useAppStore.ts +582 -0
  111. package/client/src/styles/theme.ts +508 -0
  112. package/client/src/styles/zIndex.ts +17 -0
  113. package/client/src/types/ComponentTypes.ts +39 -0
  114. package/client/src/types/EdgeCondition.ts +231 -0
  115. package/client/src/types/INodeProperties.ts +288 -0
  116. package/client/src/types/NodeTypes.ts +28 -0
  117. package/client/src/utils/formatters.ts +33 -0
  118. package/client/src/utils/googleMapsLoader.ts +140 -0
  119. package/client/src/utils/locationUtils.ts +85 -0
  120. package/client/src/utils/nodeUtils.ts +31 -0
  121. package/client/src/utils/workflow.ts +30 -0
  122. package/client/src/utils/workflowExport.ts +120 -0
  123. package/client/src/vite-env.d.ts +12 -0
  124. package/client/tailwind.config.js +60 -0
  125. package/client/tsconfig.json +25 -0
  126. package/client/tsconfig.node.json +11 -0
  127. package/client/vite.config.js +35 -0
  128. package/docker-compose.prod.yml +107 -0
  129. package/docker-compose.yml +104 -0
  130. package/docs-MachinaOs/README.md +85 -0
  131. package/docs-MachinaOs/deployment/docker.mdx +228 -0
  132. package/docs-MachinaOs/deployment/production.mdx +345 -0
  133. package/docs-MachinaOs/docs.json +75 -0
  134. package/docs-MachinaOs/faq.mdx +309 -0
  135. package/docs-MachinaOs/favicon.svg +5 -0
  136. package/docs-MachinaOs/installation.mdx +160 -0
  137. package/docs-MachinaOs/introduction.mdx +114 -0
  138. package/docs-MachinaOs/logo/dark.svg +6 -0
  139. package/docs-MachinaOs/logo/light.svg +6 -0
  140. package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
  141. package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
  142. package/docs-MachinaOs/nodes/android.mdx +411 -0
  143. package/docs-MachinaOs/nodes/overview.mdx +181 -0
  144. package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
  145. package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
  146. package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
  147. package/docs-MachinaOs/quickstart.mdx +119 -0
  148. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
  149. package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
  150. package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
  151. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
  152. package/nul +0 -0
  153. package/package.json +70 -0
  154. package/scripts/build.js +158 -0
  155. package/scripts/check-ports.ps1 +33 -0
  156. package/scripts/clean.js +40 -0
  157. package/scripts/docker.js +93 -0
  158. package/scripts/kill-port.ps1 +154 -0
  159. package/scripts/start.js +210 -0
  160. package/scripts/stop.js +325 -0
  161. package/server/.dockerignore +44 -0
  162. package/server/Dockerfile +45 -0
  163. package/server/constants.py +249 -0
  164. package/server/core/__init__.py +1 -0
  165. package/server/core/cache.py +461 -0
  166. package/server/core/config.py +128 -0
  167. package/server/core/container.py +99 -0
  168. package/server/core/database.py +1211 -0
  169. package/server/core/logging.py +314 -0
  170. package/server/main.py +289 -0
  171. package/server/middleware/__init__.py +5 -0
  172. package/server/middleware/auth.py +89 -0
  173. package/server/models/__init__.py +1 -0
  174. package/server/models/auth.py +52 -0
  175. package/server/models/cache.py +24 -0
  176. package/server/models/database.py +211 -0
  177. package/server/models/nodes.py +455 -0
  178. package/server/package.json +9 -0
  179. package/server/pyproject.toml +72 -0
  180. package/server/requirements.txt +83 -0
  181. package/server/routers/__init__.py +1 -0
  182. package/server/routers/android.py +294 -0
  183. package/server/routers/auth.py +203 -0
  184. package/server/routers/database.py +151 -0
  185. package/server/routers/maps.py +142 -0
  186. package/server/routers/nodejs_compat.py +289 -0
  187. package/server/routers/webhook.py +90 -0
  188. package/server/routers/websocket.py +2127 -0
  189. package/server/routers/whatsapp.py +761 -0
  190. package/server/routers/workflow.py +200 -0
  191. package/server/services/__init__.py +1 -0
  192. package/server/services/ai.py +2415 -0
  193. package/server/services/android/__init__.py +27 -0
  194. package/server/services/android/broadcaster.py +114 -0
  195. package/server/services/android/client.py +608 -0
  196. package/server/services/android/manager.py +78 -0
  197. package/server/services/android/protocol.py +165 -0
  198. package/server/services/android_service.py +588 -0
  199. package/server/services/auth.py +131 -0
  200. package/server/services/chat_client.py +160 -0
  201. package/server/services/deployment/__init__.py +12 -0
  202. package/server/services/deployment/manager.py +706 -0
  203. package/server/services/deployment/state.py +47 -0
  204. package/server/services/deployment/triggers.py +275 -0
  205. package/server/services/event_waiter.py +785 -0
  206. package/server/services/execution/__init__.py +77 -0
  207. package/server/services/execution/cache.py +769 -0
  208. package/server/services/execution/conditions.py +373 -0
  209. package/server/services/execution/dlq.py +132 -0
  210. package/server/services/execution/executor.py +1351 -0
  211. package/server/services/execution/models.py +531 -0
  212. package/server/services/execution/recovery.py +235 -0
  213. package/server/services/handlers/__init__.py +126 -0
  214. package/server/services/handlers/ai.py +355 -0
  215. package/server/services/handlers/android.py +260 -0
  216. package/server/services/handlers/code.py +278 -0
  217. package/server/services/handlers/document.py +598 -0
  218. package/server/services/handlers/http.py +193 -0
  219. package/server/services/handlers/polyglot.py +105 -0
  220. package/server/services/handlers/tools.py +845 -0
  221. package/server/services/handlers/triggers.py +107 -0
  222. package/server/services/handlers/utility.py +822 -0
  223. package/server/services/handlers/whatsapp.py +476 -0
  224. package/server/services/maps.py +289 -0
  225. package/server/services/memory_store.py +103 -0
  226. package/server/services/node_executor.py +375 -0
  227. package/server/services/parameter_resolver.py +218 -0
  228. package/server/services/polyglot_client.py +169 -0
  229. package/server/services/scheduler.py +155 -0
  230. package/server/services/skill_loader.py +417 -0
  231. package/server/services/status_broadcaster.py +826 -0
  232. package/server/services/temporal/__init__.py +23 -0
  233. package/server/services/temporal/activities.py +344 -0
  234. package/server/services/temporal/client.py +76 -0
  235. package/server/services/temporal/executor.py +147 -0
  236. package/server/services/temporal/worker.py +251 -0
  237. package/server/services/temporal/workflow.py +355 -0
  238. package/server/services/temporal/ws_client.py +236 -0
  239. package/server/services/text.py +111 -0
  240. package/server/services/user_auth.py +172 -0
  241. package/server/services/websocket_client.py +29 -0
  242. package/server/services/workflow.py +597 -0
  243. package/server/skills/android-skill/SKILL.md +82 -0
  244. package/server/skills/assistant-personality/SKILL.md +45 -0
  245. package/server/skills/code-skill/SKILL.md +140 -0
  246. package/server/skills/http-skill/SKILL.md +161 -0
  247. package/server/skills/maps-skill/SKILL.md +170 -0
  248. package/server/skills/memory-skill/SKILL.md +154 -0
  249. package/server/skills/scheduler-skill/SKILL.md +84 -0
  250. package/server/skills/whatsapp-skill/SKILL.md +283 -0
  251. package/server/uv.lock +2916 -0
  252. package/server/whatsapp-rpc/.dockerignore +30 -0
  253. package/server/whatsapp-rpc/Dockerfile +44 -0
  254. package/server/whatsapp-rpc/Dockerfile.web +17 -0
  255. package/server/whatsapp-rpc/README.md +139 -0
  256. package/server/whatsapp-rpc/cli.js +95 -0
  257. package/server/whatsapp-rpc/configs/config.yaml +7 -0
  258. package/server/whatsapp-rpc/docker-compose.yml +35 -0
  259. package/server/whatsapp-rpc/docs/API.md +410 -0
  260. package/server/whatsapp-rpc/go.mod +67 -0
  261. package/server/whatsapp-rpc/go.sum +203 -0
  262. package/server/whatsapp-rpc/package.json +30 -0
  263. package/server/whatsapp-rpc/schema.json +1294 -0
  264. package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
  265. package/server/whatsapp-rpc/scripts/cli.js +162 -0
  266. package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
  267. package/server/whatsapp-rpc/src/go/config/config.go +49 -0
  268. package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
  269. package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
  270. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
  271. package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
  272. package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
  273. package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
  274. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
  275. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
  276. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
  277. package/server/whatsapp-rpc/web/app.py +609 -0
  278. package/server/whatsapp-rpc/web/requirements.txt +6 -0
  279. package/server/whatsapp-rpc/web/rpc_client.py +427 -0
  280. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
  281. package/server/whatsapp-rpc/web/templates/base.html +150 -0
  282. package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
  283. package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
  284. package/server/whatsapp-rpc/web/templates/groups.html +328 -0
  285. package/server/whatsapp-rpc/web/templates/messages.html +465 -0
  286. package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
  287. package/server/whatsapp-rpc/web/templates/send.html +259 -0
  288. package/server/whatsapp-rpc/web/templates/settings.html +459 -0
@@ -0,0 +1,531 @@
1
+ """Execution engine state models.
2
+
3
+ Based on Netflix Conductor task lifecycle and Prefect 3.0 patterns.
4
+ All models are JSON-serializable for Redis persistence and cross-runtime portability.
5
+ """
6
+
7
+ import hashlib
8
+ import json
9
+ import time
10
+ import uuid
11
+ from dataclasses import dataclass, field, asdict
12
+ from datetime import datetime
13
+ from enum import Enum
14
+ from typing import Dict, Any, List, Optional
15
+
16
+
17
+ class TaskStatus(str, Enum):
18
+ """Task execution states (Conductor-style lifecycle).
19
+
20
+ State transitions:
21
+ PENDING -> SCHEDULED -> RUNNING -> COMPLETED
22
+ -> FAILED
23
+ -> CANCELLED
24
+ CACHED (Prefect pattern - result from cache, no execution)
25
+ """
26
+ PENDING = "pending" # Created, not yet scheduled
27
+ SCHEDULED = "scheduled" # In task queue, waiting for worker
28
+ RUNNING = "running" # Worker executing
29
+ COMPLETED = "completed" # Success, result cached
30
+ FAILED = "failed" # Error, may retry
31
+ CACHED = "cached" # Result from cache (Prefect pattern)
32
+ CANCELLED = "cancelled" # User cancelled
33
+ WAITING = "waiting" # Waiting for external event (triggers)
34
+ SKIPPED = "skipped" # Skipped due to condition
35
+
36
+
37
+ class WorkflowStatus(str, Enum):
38
+ """Workflow execution states."""
39
+ PENDING = "pending" # Created, not started
40
+ RUNNING = "running" # At least one node executing
41
+ PAUSED = "paused" # User paused
42
+ COMPLETED = "completed" # All nodes completed successfully
43
+ FAILED = "failed" # Execution failed
44
+ CANCELLED = "cancelled" # User cancelled
45
+
46
+
47
+ @dataclass
48
+ class RetryPolicy:
49
+ """Retry configuration for node execution.
50
+
51
+ Implements exponential backoff with configurable limits.
52
+ Delay formula: min(initial_delay * (backoff_multiplier ^ attempt), max_delay)
53
+ """
54
+ max_attempts: int = 3
55
+ initial_delay: float = 1.0 # seconds
56
+ max_delay: float = 60.0 # seconds
57
+ backoff_multiplier: float = 2.0
58
+ retry_on_timeout: bool = True
59
+ retry_on_connection_error: bool = True
60
+ retry_on_server_error: bool = True # 5xx errors
61
+
62
+ def calculate_delay(self, attempt: int) -> float:
63
+ """Calculate delay before next retry attempt.
64
+
65
+ Args:
66
+ attempt: Current attempt number (0-indexed)
67
+
68
+ Returns:
69
+ Delay in seconds before next attempt
70
+ """
71
+ delay = self.initial_delay * (self.backoff_multiplier ** attempt)
72
+ return min(delay, self.max_delay)
73
+
74
+ def should_retry(self, error: str, attempt: int) -> bool:
75
+ """Determine if execution should be retried.
76
+
77
+ Args:
78
+ error: Error message from failed execution
79
+ attempt: Current attempt number (0-indexed)
80
+
81
+ Returns:
82
+ True if should retry, False otherwise
83
+ """
84
+ if attempt >= self.max_attempts:
85
+ return False
86
+
87
+ error_lower = error.lower()
88
+
89
+ if self.retry_on_timeout and "timeout" in error_lower:
90
+ return True
91
+ if self.retry_on_connection_error and ("connection" in error_lower or "connect" in error_lower):
92
+ return True
93
+ if self.retry_on_server_error and ("500" in error or "502" in error or "503" in error or "504" in error):
94
+ return True
95
+
96
+ return False
97
+
98
+ def to_dict(self) -> Dict[str, Any]:
99
+ """Convert to JSON-serializable dict."""
100
+ return {
101
+ "max_attempts": self.max_attempts,
102
+ "initial_delay": self.initial_delay,
103
+ "max_delay": self.max_delay,
104
+ "backoff_multiplier": self.backoff_multiplier,
105
+ "retry_on_timeout": self.retry_on_timeout,
106
+ "retry_on_connection_error": self.retry_on_connection_error,
107
+ "retry_on_server_error": self.retry_on_server_error,
108
+ }
109
+
110
+ @classmethod
111
+ def from_dict(cls, data: Dict[str, Any]) -> "RetryPolicy":
112
+ """Create from dict."""
113
+ return cls(
114
+ max_attempts=data.get("max_attempts", 3),
115
+ initial_delay=data.get("initial_delay", 1.0),
116
+ max_delay=data.get("max_delay", 60.0),
117
+ backoff_multiplier=data.get("backoff_multiplier", 2.0),
118
+ retry_on_timeout=data.get("retry_on_timeout", True),
119
+ retry_on_connection_error=data.get("retry_on_connection_error", True),
120
+ retry_on_server_error=data.get("retry_on_server_error", True),
121
+ )
122
+
123
+
124
+ # Default retry policies for different node types
125
+ DEFAULT_RETRY_POLICIES: Dict[str, RetryPolicy] = {
126
+ "httpRequest": RetryPolicy(max_attempts=3, initial_delay=2.0),
127
+ "webhookTrigger": RetryPolicy(max_attempts=1), # Don't retry triggers
128
+ "whatsappReceive": RetryPolicy(max_attempts=1), # Don't retry triggers
129
+ "aiAgent": RetryPolicy(max_attempts=2, initial_delay=5.0, max_delay=30.0),
130
+ "openaiChatModel": RetryPolicy(max_attempts=2, initial_delay=5.0),
131
+ "anthropicChatModel": RetryPolicy(max_attempts=2, initial_delay=5.0),
132
+ "googleChatModel": RetryPolicy(max_attempts=2, initial_delay=5.0),
133
+ }
134
+
135
+
136
+ def get_retry_policy(node_type: str, custom_policy: Dict = None) -> RetryPolicy:
137
+ """Get retry policy for a node type.
138
+
139
+ Args:
140
+ node_type: The node type string
141
+ custom_policy: Optional custom policy dict from node parameters
142
+
143
+ Returns:
144
+ RetryPolicy instance
145
+ """
146
+ if custom_policy:
147
+ return RetryPolicy.from_dict(custom_policy)
148
+ return DEFAULT_RETRY_POLICIES.get(node_type, RetryPolicy())
149
+
150
+
151
+ @dataclass
152
+ class DLQEntry:
153
+ """Dead Letter Queue entry for failed node executions.
154
+
155
+ Stores failed execution details for manual review and replay.
156
+ """
157
+ id: str
158
+ execution_id: str
159
+ workflow_id: str
160
+ node_id: str
161
+ node_type: str
162
+ error: str
163
+ inputs: Dict[str, Any]
164
+ retry_count: int
165
+ created_at: float = field(default_factory=time.time)
166
+ last_error_at: float = field(default_factory=time.time)
167
+
168
+ def to_dict(self) -> Dict[str, Any]:
169
+ """Convert to JSON-serializable dict."""
170
+ return {
171
+ "id": self.id,
172
+ "execution_id": self.execution_id,
173
+ "workflow_id": self.workflow_id,
174
+ "node_id": self.node_id,
175
+ "node_type": self.node_type,
176
+ "error": self.error,
177
+ "inputs": self.inputs,
178
+ "retry_count": self.retry_count,
179
+ "created_at": self.created_at,
180
+ "last_error_at": self.last_error_at,
181
+ }
182
+
183
+ @classmethod
184
+ def from_dict(cls, data: Dict[str, Any]) -> "DLQEntry":
185
+ """Create from dict."""
186
+ return cls(
187
+ id=data["id"],
188
+ execution_id=data["execution_id"],
189
+ workflow_id=data["workflow_id"],
190
+ node_id=data["node_id"],
191
+ node_type=data["node_type"],
192
+ error=data["error"],
193
+ inputs=data.get("inputs", {}),
194
+ retry_count=data.get("retry_count", 0),
195
+ created_at=data.get("created_at", time.time()),
196
+ last_error_at=data.get("last_error_at", time.time()),
197
+ )
198
+
199
+ @classmethod
200
+ def create(cls, ctx: "ExecutionContext", node_exec: "NodeExecution",
201
+ inputs: Dict[str, Any]) -> "DLQEntry":
202
+ """Factory method to create DLQ entry from failed execution."""
203
+ return cls(
204
+ id=str(uuid.uuid4()),
205
+ execution_id=ctx.execution_id,
206
+ workflow_id=ctx.workflow_id,
207
+ node_id=node_exec.node_id,
208
+ node_type=node_exec.node_type,
209
+ error=node_exec.error or "Unknown error",
210
+ inputs=inputs,
211
+ retry_count=node_exec.retry_count,
212
+ )
213
+
214
+
215
+ @dataclass
216
+ class NodeExecution:
217
+ """Tracks execution state for a single node.
218
+
219
+ Prefect-style: includes input hash for cache lookup.
220
+ """
221
+ node_id: str
222
+ node_type: str
223
+ status: TaskStatus = TaskStatus.PENDING
224
+ input_hash: Optional[str] = None # For cache lookup
225
+ output: Optional[Dict[str, Any]] = None
226
+ error: Optional[str] = None
227
+ started_at: Optional[float] = None
228
+ completed_at: Optional[float] = None
229
+ retry_count: int = 0
230
+
231
+ def to_dict(self) -> Dict[str, Any]:
232
+ """Convert to JSON-serializable dict."""
233
+ return {
234
+ "node_id": self.node_id,
235
+ "node_type": self.node_type,
236
+ "status": self.status.value,
237
+ "input_hash": self.input_hash,
238
+ "output": self.output,
239
+ "error": self.error,
240
+ "started_at": self.started_at,
241
+ "completed_at": self.completed_at,
242
+ "retry_count": self.retry_count,
243
+ }
244
+
245
+ @classmethod
246
+ def from_dict(cls, data: Dict[str, Any]) -> "NodeExecution":
247
+ """Create from dict (Redis deserialization)."""
248
+ return cls(
249
+ node_id=data["node_id"],
250
+ node_type=data["node_type"],
251
+ status=TaskStatus(data["status"]),
252
+ input_hash=data.get("input_hash"),
253
+ output=data.get("output"),
254
+ error=data.get("error"),
255
+ started_at=data.get("started_at"),
256
+ completed_at=data.get("completed_at"),
257
+ retry_count=data.get("retry_count", 0),
258
+ )
259
+
260
+
261
+ @dataclass
262
+ class ExecutionContext:
263
+ """Isolated execution context for a workflow run.
264
+
265
+ Replaces global _deployment_running flag.
266
+ Each workflow execution gets its own context with isolated state.
267
+
268
+ Conductor pattern: workflow_id identifies the workflow definition,
269
+ execution_id identifies this specific run.
270
+ """
271
+ execution_id: str
272
+ workflow_id: str
273
+ status: WorkflowStatus = WorkflowStatus.PENDING
274
+ session_id: str = "default"
275
+
276
+ # Node states and outputs
277
+ node_executions: Dict[str, NodeExecution] = field(default_factory=dict)
278
+ outputs: Dict[str, Any] = field(default_factory=dict)
279
+
280
+ # DAG structure (cached for parallel batch detection)
281
+ nodes: List[Dict[str, Any]] = field(default_factory=list)
282
+ edges: List[Dict[str, Any]] = field(default_factory=list)
283
+
284
+ # Execution tracking
285
+ execution_order: List[str] = field(default_factory=list)
286
+ current_layer: int = 0
287
+ checkpoints: List[str] = field(default_factory=list)
288
+
289
+ # Timing
290
+ created_at: float = field(default_factory=time.time)
291
+ updated_at: float = field(default_factory=time.time)
292
+ started_at: Optional[float] = None
293
+ completed_at: Optional[float] = None
294
+
295
+ # Error tracking
296
+ errors: List[Dict[str, Any]] = field(default_factory=list)
297
+
298
+ @classmethod
299
+ def create(cls, workflow_id: str, session_id: str = "default",
300
+ nodes: List[Dict] = None, edges: List[Dict] = None) -> "ExecutionContext":
301
+ """Factory method to create new execution context.
302
+
303
+ Supports pre-executed nodes (marked with _pre_executed=True) for
304
+ event-driven execution where trigger nodes are already complete.
305
+
306
+ Config nodes (memory, tools, model configs) are excluded from execution
307
+ as they provide configuration to other nodes via special handles.
308
+
309
+ Toolkit sub-nodes (nodes connected TO a toolkit like androidTool) are also
310
+ excluded - they execute only when called via the toolkit's tool interface.
311
+ """
312
+ from constants import CONFIG_NODE_TYPES, TOOLKIT_NODE_TYPES
313
+
314
+ execution_id = str(uuid.uuid4())
315
+ ctx = cls(
316
+ execution_id=execution_id,
317
+ workflow_id=workflow_id,
318
+ session_id=session_id,
319
+ nodes=nodes or [],
320
+ edges=edges or [],
321
+ )
322
+
323
+ # Find toolkit sub-nodes (nodes that connect TO a toolkit node)
324
+ # These should only execute when called via the toolkit, not as workflow nodes
325
+ toolkit_node_ids = {n.get("id") for n in (nodes or []) if n.get("type") in TOOLKIT_NODE_TYPES}
326
+
327
+ # Find AI Agent nodes (both aiAgent and chatAgent have config handles)
328
+ ai_agent_node_ids = {n.get("id") for n in (nodes or []) if n.get("type") in ('aiAgent', 'chatAgent')}
329
+
330
+ subnode_ids: set = set()
331
+ for edge in (edges or []):
332
+ source = edge.get("source")
333
+ target = edge.get("target")
334
+ target_handle = edge.get("targetHandle")
335
+
336
+ # Any node that connects TO a toolkit is a sub-node
337
+ if target in toolkit_node_ids and source:
338
+ subnode_ids.add(source)
339
+
340
+ # Nodes connected to AI Agent/Chat Agent config handles are sub-nodes
341
+ # These handles: input-memory, input-tools, input-skill
342
+ if target in ai_agent_node_ids and source and target_handle:
343
+ if target_handle in ('input-memory', 'input-tools', 'input-skill'):
344
+ subnode_ids.add(source)
345
+
346
+ # Initialize node executions for all nodes (excluding config nodes and sub-nodes)
347
+ for node in (nodes or []):
348
+ node_id = node.get("id")
349
+ node_type = node.get("type", "unknown")
350
+
351
+ # Skip config nodes - they don't execute independently
352
+ # They provide configuration to other nodes via special handles
353
+ if node_type in CONFIG_NODE_TYPES:
354
+ continue
355
+
356
+ # Skip toolkit sub-nodes - they execute only via toolkit tool calls
357
+ if node_id in subnode_ids:
358
+ continue
359
+
360
+ # Check if node is pre-executed (e.g., trigger that already fired)
361
+ if node.get("_pre_executed"):
362
+ # Mark as COMPLETED with trigger output
363
+ trigger_output = node.get("_trigger_output", {})
364
+ node_exec = NodeExecution(
365
+ node_id=node_id,
366
+ node_type=node_type,
367
+ status=TaskStatus.COMPLETED,
368
+ output=trigger_output,
369
+ completed_at=time.time(),
370
+ )
371
+ ctx.outputs[node_id] = trigger_output
372
+ ctx.checkpoints.append(node_id)
373
+ else:
374
+ node_exec = NodeExecution(
375
+ node_id=node_id,
376
+ node_type=node_type,
377
+ )
378
+
379
+ ctx.node_executions[node_id] = node_exec
380
+
381
+ return ctx
382
+
383
+ def get_node_status(self, node_id: str) -> Optional[TaskStatus]:
384
+ """Get status for a specific node."""
385
+ node_exec = self.node_executions.get(node_id)
386
+ return node_exec.status if node_exec else None
387
+
388
+ def set_node_status(self, node_id: str, status: TaskStatus,
389
+ output: Dict = None, error: str = None) -> None:
390
+ """Update node execution status."""
391
+ if node_id not in self.node_executions:
392
+ return
393
+
394
+ node_exec = self.node_executions[node_id]
395
+ node_exec.status = status
396
+ self.updated_at = time.time()
397
+
398
+ if status == TaskStatus.RUNNING:
399
+ node_exec.started_at = time.time()
400
+ elif status in (TaskStatus.COMPLETED, TaskStatus.CACHED):
401
+ node_exec.completed_at = time.time()
402
+ if output:
403
+ node_exec.output = output
404
+ self.outputs[node_id] = output
405
+ elif status == TaskStatus.SKIPPED:
406
+ # Skipped due to conditional branching - mark as completed but no output
407
+ node_exec.completed_at = time.time()
408
+ elif status == TaskStatus.FAILED:
409
+ node_exec.completed_at = time.time()
410
+ if error:
411
+ node_exec.error = error
412
+ self.errors.append({
413
+ "node_id": node_id,
414
+ "error": error,
415
+ "timestamp": time.time()
416
+ })
417
+
418
+ def add_checkpoint(self, node_id: str) -> None:
419
+ """Add checkpoint after node completion (for recovery)."""
420
+ self.checkpoints.append(node_id)
421
+ self.updated_at = time.time()
422
+
423
+ def get_completed_nodes(self) -> List[str]:
424
+ """Get list of completed node IDs."""
425
+ return [
426
+ node_id for node_id, node_exec in self.node_executions.items()
427
+ if node_exec.status in (TaskStatus.COMPLETED, TaskStatus.CACHED, TaskStatus.SKIPPED)
428
+ ]
429
+
430
+ def get_pending_nodes(self) -> List[str]:
431
+ """Get list of pending node IDs."""
432
+ return [
433
+ node_id for node_id, node_exec in self.node_executions.items()
434
+ if node_exec.status == TaskStatus.PENDING
435
+ ]
436
+
437
+ def all_nodes_complete(self) -> bool:
438
+ """Check if all nodes are complete."""
439
+ for node_exec in self.node_executions.values():
440
+ if node_exec.status not in (TaskStatus.COMPLETED, TaskStatus.CACHED,
441
+ TaskStatus.SKIPPED, TaskStatus.CANCELLED):
442
+ return False
443
+ return True
444
+
445
+ def to_dict(self) -> Dict[str, Any]:
446
+ """Convert to JSON-serializable dict for Redis storage."""
447
+ return {
448
+ "execution_id": self.execution_id,
449
+ "workflow_id": self.workflow_id,
450
+ "status": self.status.value,
451
+ "session_id": self.session_id,
452
+ "node_executions": {
453
+ k: v.to_dict() for k, v in self.node_executions.items()
454
+ },
455
+ "outputs": self.outputs,
456
+ "execution_order": self.execution_order,
457
+ "current_layer": self.current_layer,
458
+ "checkpoints": self.checkpoints,
459
+ "created_at": self.created_at,
460
+ "updated_at": self.updated_at,
461
+ "started_at": self.started_at,
462
+ "completed_at": self.completed_at,
463
+ "errors": self.errors,
464
+ # Don't store full nodes/edges - too large
465
+ "node_count": len(self.nodes),
466
+ "edge_count": len(self.edges),
467
+ }
468
+
469
+ @classmethod
470
+ def from_dict(cls, data: Dict[str, Any], nodes: List[Dict] = None,
471
+ edges: List[Dict] = None) -> "ExecutionContext":
472
+ """Create from dict (Redis deserialization)."""
473
+ ctx = cls(
474
+ execution_id=data["execution_id"],
475
+ workflow_id=data["workflow_id"],
476
+ status=WorkflowStatus(data["status"]),
477
+ session_id=data.get("session_id", "default"),
478
+ nodes=nodes or [],
479
+ edges=edges or [],
480
+ execution_order=data.get("execution_order", []),
481
+ current_layer=data.get("current_layer", 0),
482
+ checkpoints=data.get("checkpoints", []),
483
+ created_at=data.get("created_at", time.time()),
484
+ updated_at=data.get("updated_at", time.time()),
485
+ started_at=data.get("started_at"),
486
+ completed_at=data.get("completed_at"),
487
+ errors=data.get("errors", []),
488
+ )
489
+
490
+ # Restore node executions
491
+ for node_id, node_data in data.get("node_executions", {}).items():
492
+ ctx.node_executions[node_id] = NodeExecution.from_dict(node_data)
493
+
494
+ # Restore outputs
495
+ ctx.outputs = data.get("outputs", {})
496
+
497
+ return ctx
498
+
499
+ def to_json(self) -> str:
500
+ """Serialize to JSON string."""
501
+ return json.dumps(self.to_dict())
502
+
503
+ @classmethod
504
+ def from_json(cls, json_str: str, nodes: List[Dict] = None,
505
+ edges: List[Dict] = None) -> "ExecutionContext":
506
+ """Deserialize from JSON string."""
507
+ data = json.loads(json_str)
508
+ return cls.from_dict(data, nodes, edges)
509
+
510
+
511
+ def hash_inputs(inputs: Dict[str, Any]) -> str:
512
+ """Generate deterministic hash of inputs for cache key (Prefect pattern).
513
+
514
+ Args:
515
+ inputs: Dictionary of input parameters
516
+
517
+ Returns:
518
+ SHA256 hash of canonicalized inputs
519
+ """
520
+ # Canonical JSON (sorted keys, no extra whitespace)
521
+ canonical = json.dumps(inputs, sort_keys=True, separators=(",", ":"))
522
+ return hashlib.sha256(canonical.encode()).hexdigest()[:16]
523
+
524
+
525
+ def generate_cache_key(execution_id: str, node_id: str, inputs: Dict[str, Any]) -> str:
526
+ """Generate cache key for node result (Prefect pattern).
527
+
528
+ Format: result:{execution_id}:{node_id}:{input_hash}
529
+ """
530
+ input_hash = hash_inputs(inputs)
531
+ return f"result:{execution_id}:{node_id}:{input_hash}"