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,373 @@
1
+ """Condition evaluation for runtime conditional branching.
2
+
3
+ Evaluates edge conditions against node outputs to determine
4
+ which paths to follow in a workflow (Prefect-style dynamic branching).
5
+
6
+ Supported operators:
7
+ - eq: Equal (==)
8
+ - neq: Not equal (!=)
9
+ - gt: Greater than (>)
10
+ - lt: Less than (<)
11
+ - gte: Greater than or equal (>=)
12
+ - lte: Less than or equal (<=)
13
+ - contains: String/list contains value
14
+ - not_contains: String/list does not contain value
15
+ - exists: Field exists and is not None
16
+ - not_exists: Field does not exist or is None
17
+ - is_empty: Field is empty (None, "", [], {})
18
+ - is_not_empty: Field is not empty
19
+ - matches: Regex pattern match
20
+ - in: Value is in list
21
+ - not_in: Value is not in list
22
+ - starts_with: String starts with value
23
+ - ends_with: String ends with value
24
+ """
25
+
26
+ import re
27
+ from typing import Dict, Any, Optional, List, Union
28
+
29
+ from core.logging import get_logger
30
+
31
+ logger = get_logger(__name__)
32
+
33
+
34
+ # Type alias for condition dict
35
+ ConditionDict = Dict[str, Any]
36
+
37
+
38
+ def get_nested_value(data: Dict[str, Any], field_path: str) -> Any:
39
+ """Get a nested value from a dictionary using dot notation.
40
+
41
+ Args:
42
+ data: Dictionary to extract value from
43
+ field_path: Dot-separated path (e.g., "result.status", "items.0.name")
44
+
45
+ Returns:
46
+ Value at path or None if not found
47
+
48
+ Examples:
49
+ >>> get_nested_value({"result": {"status": "ok"}}, "result.status")
50
+ "ok"
51
+ >>> get_nested_value({"items": [{"name": "a"}]}, "items.0.name")
52
+ "a"
53
+ """
54
+ if not data or not field_path:
55
+ return None
56
+
57
+ parts = field_path.split('.')
58
+ current = data
59
+
60
+ for part in parts:
61
+ if current is None:
62
+ return None
63
+
64
+ # Handle array index
65
+ if part.isdigit():
66
+ index = int(part)
67
+ if isinstance(current, (list, tuple)) and 0 <= index < len(current):
68
+ current = current[index]
69
+ else:
70
+ return None
71
+ # Handle dict key
72
+ elif isinstance(current, dict):
73
+ current = current.get(part)
74
+ else:
75
+ return None
76
+
77
+ return current
78
+
79
+
80
+ def evaluate_condition(condition: ConditionDict, output: Dict[str, Any]) -> bool:
81
+ """Evaluate an edge condition against node output.
82
+
83
+ Args:
84
+ condition: Condition dict with field, operator, value
85
+ {
86
+ "field": "status", # Output field to check
87
+ "operator": "eq", # Comparison operator
88
+ "value": "success" # Value to compare against
89
+ }
90
+ output: Node execution output dict
91
+
92
+ Returns:
93
+ True if condition matches, False otherwise
94
+ """
95
+ if not condition:
96
+ return True # No condition = always follow
97
+
98
+ field = condition.get("field", "")
99
+ operator = condition.get("operator", "eq")
100
+ target_value = condition.get("value")
101
+
102
+ # Get the actual value from output
103
+ actual_value = get_nested_value(output, field)
104
+
105
+ logger.debug("Evaluating condition",
106
+ field=field,
107
+ operator=operator,
108
+ target=target_value,
109
+ actual=actual_value)
110
+
111
+ try:
112
+ result = _evaluate_operator(operator, actual_value, target_value)
113
+ logger.debug("Condition result", result=result)
114
+ return result
115
+ except Exception as e:
116
+ logger.warning("Condition evaluation error",
117
+ field=field,
118
+ operator=operator,
119
+ error=str(e))
120
+ return False
121
+
122
+
123
+ def _evaluate_operator(operator: str, actual: Any, target: Any) -> bool:
124
+ """Evaluate a single operator.
125
+
126
+ Args:
127
+ operator: Operator name
128
+ actual: Actual value from output
129
+ target: Target value to compare
130
+
131
+ Returns:
132
+ Comparison result
133
+ """
134
+ # Equality operators
135
+ if operator == "eq":
136
+ return actual == target
137
+
138
+ elif operator == "neq":
139
+ return actual != target
140
+
141
+ # Comparison operators (numeric)
142
+ elif operator == "gt":
143
+ return _safe_compare(actual, target, lambda a, b: a > b)
144
+
145
+ elif operator == "lt":
146
+ return _safe_compare(actual, target, lambda a, b: a < b)
147
+
148
+ elif operator == "gte":
149
+ return _safe_compare(actual, target, lambda a, b: a >= b)
150
+
151
+ elif operator == "lte":
152
+ return _safe_compare(actual, target, lambda a, b: a <= b)
153
+
154
+ # String/list contains
155
+ elif operator == "contains":
156
+ if actual is None:
157
+ return False
158
+ if isinstance(actual, str):
159
+ return str(target) in actual
160
+ elif isinstance(actual, (list, tuple)):
161
+ return target in actual
162
+ elif isinstance(actual, dict):
163
+ return target in actual
164
+ return False
165
+
166
+ elif operator == "not_contains":
167
+ return not _evaluate_operator("contains", actual, target)
168
+
169
+ # Existence checks
170
+ elif operator == "exists":
171
+ return actual is not None
172
+
173
+ elif operator == "not_exists":
174
+ return actual is None
175
+
176
+ # Empty checks
177
+ elif operator == "is_empty":
178
+ if actual is None:
179
+ return True
180
+ if isinstance(actual, (str, list, dict, tuple)):
181
+ return len(actual) == 0
182
+ return False
183
+
184
+ elif operator == "is_not_empty":
185
+ return not _evaluate_operator("is_empty", actual, target)
186
+
187
+ # Regex match
188
+ elif operator == "matches":
189
+ if actual is None or target is None:
190
+ return False
191
+ try:
192
+ return bool(re.search(str(target), str(actual)))
193
+ except re.error:
194
+ logger.warning("Invalid regex pattern", pattern=target)
195
+ return False
196
+
197
+ # List membership
198
+ elif operator == "in":
199
+ if not isinstance(target, (list, tuple)):
200
+ return actual == target
201
+ return actual in target
202
+
203
+ elif operator == "not_in":
204
+ return not _evaluate_operator("in", actual, target)
205
+
206
+ # String prefix/suffix
207
+ elif operator == "starts_with":
208
+ if actual is None or target is None:
209
+ return False
210
+ return str(actual).startswith(str(target))
211
+
212
+ elif operator == "ends_with":
213
+ if actual is None or target is None:
214
+ return False
215
+ return str(actual).endswith(str(target))
216
+
217
+ # Boolean checks
218
+ elif operator == "is_true":
219
+ return actual is True or actual == "true" or actual == 1
220
+
221
+ elif operator == "is_false":
222
+ return actual is False or actual == "false" or actual == 0
223
+
224
+ # Type checks
225
+ elif operator == "is_string":
226
+ return isinstance(actual, str)
227
+
228
+ elif operator == "is_number":
229
+ return isinstance(actual, (int, float)) and not isinstance(actual, bool)
230
+
231
+ elif operator == "is_boolean":
232
+ return isinstance(actual, bool)
233
+
234
+ elif operator == "is_array":
235
+ return isinstance(actual, (list, tuple))
236
+
237
+ elif operator == "is_object":
238
+ return isinstance(actual, dict)
239
+
240
+ else:
241
+ logger.warning("Unknown operator", operator=operator)
242
+ return False
243
+
244
+
245
+ def _safe_compare(actual: Any, target: Any, comparator) -> bool:
246
+ """Safely compare two values, handling type coercion.
247
+
248
+ Args:
249
+ actual: Actual value
250
+ target: Target value
251
+ comparator: Comparison function
252
+
253
+ Returns:
254
+ Comparison result, False if comparison impossible
255
+ """
256
+ if actual is None or target is None:
257
+ return False
258
+
259
+ # Try numeric comparison first
260
+ try:
261
+ return comparator(float(actual), float(target))
262
+ except (ValueError, TypeError):
263
+ pass
264
+
265
+ # Fall back to string comparison
266
+ try:
267
+ return comparator(str(actual), str(target))
268
+ except (ValueError, TypeError):
269
+ return False
270
+
271
+
272
+ def evaluate_conditions(conditions: List[ConditionDict], output: Dict[str, Any],
273
+ logic: str = "and") -> bool:
274
+ """Evaluate multiple conditions with AND/OR logic.
275
+
276
+ Args:
277
+ conditions: List of condition dicts
278
+ output: Node execution output
279
+ logic: "and" (all must match) or "or" (any must match)
280
+
281
+ Returns:
282
+ Combined evaluation result
283
+ """
284
+ if not conditions:
285
+ return True
286
+
287
+ results = [evaluate_condition(c, output) for c in conditions]
288
+
289
+ if logic == "or":
290
+ return any(results)
291
+ else: # "and"
292
+ return all(results)
293
+
294
+
295
+ def decide_next_edges(edges: List[Dict[str, Any]], source_node_id: str,
296
+ output: Dict[str, Any]) -> List[str]:
297
+ """Determine which edges to follow based on conditions.
298
+
299
+ Args:
300
+ edges: All edges in workflow
301
+ source_node_id: ID of the node that just completed
302
+ output: Output from the completed node
303
+
304
+ Returns:
305
+ List of target node IDs to execute next
306
+ """
307
+ next_nodes = []
308
+ unconditional_edges = []
309
+ conditional_edges = []
310
+
311
+ # Separate edges by type
312
+ for edge in edges:
313
+ if edge.get("source") != source_node_id:
314
+ continue
315
+
316
+ condition = edge.get("data", {}).get("condition")
317
+ if condition:
318
+ conditional_edges.append(edge)
319
+ else:
320
+ unconditional_edges.append(edge)
321
+
322
+ # If there are conditional edges, evaluate them
323
+ if conditional_edges:
324
+ for edge in conditional_edges:
325
+ condition = edge.get("data", {}).get("condition")
326
+ if evaluate_condition(condition, output):
327
+ next_nodes.append(edge["target"])
328
+ logger.info("Conditional edge matched",
329
+ source=source_node_id,
330
+ target=edge["target"],
331
+ condition=condition)
332
+
333
+ # If no conditional edges matched, fall through to unconditional
334
+ if not next_nodes and unconditional_edges:
335
+ logger.info("No conditional edges matched, using unconditional",
336
+ source=source_node_id)
337
+ for edge in unconditional_edges:
338
+ next_nodes.append(edge["target"])
339
+ else:
340
+ # No conditions - follow all unconditional edges
341
+ for edge in unconditional_edges:
342
+ next_nodes.append(edge["target"])
343
+
344
+ return next_nodes
345
+
346
+
347
+ # Operator metadata for frontend UI
348
+ OPERATORS = {
349
+ "eq": {"label": "Equals", "description": "Value equals target", "requires_value": True},
350
+ "neq": {"label": "Not Equals", "description": "Value does not equal target", "requires_value": True},
351
+ "gt": {"label": "Greater Than", "description": "Value is greater than target", "requires_value": True},
352
+ "lt": {"label": "Less Than", "description": "Value is less than target", "requires_value": True},
353
+ "gte": {"label": "Greater or Equal", "description": "Value is greater than or equal to target", "requires_value": True},
354
+ "lte": {"label": "Less or Equal", "description": "Value is less than or equal to target", "requires_value": True},
355
+ "contains": {"label": "Contains", "description": "String/list contains value", "requires_value": True},
356
+ "not_contains": {"label": "Does Not Contain", "description": "String/list does not contain value", "requires_value": True},
357
+ "exists": {"label": "Exists", "description": "Field exists and is not null", "requires_value": False},
358
+ "not_exists": {"label": "Does Not Exist", "description": "Field does not exist or is null", "requires_value": False},
359
+ "is_empty": {"label": "Is Empty", "description": "Value is empty (null, '', [], {})", "requires_value": False},
360
+ "is_not_empty": {"label": "Is Not Empty", "description": "Value is not empty", "requires_value": False},
361
+ "matches": {"label": "Matches Regex", "description": "Value matches regex pattern", "requires_value": True},
362
+ "in": {"label": "In List", "description": "Value is in list", "requires_value": True},
363
+ "not_in": {"label": "Not In List", "description": "Value is not in list", "requires_value": True},
364
+ "starts_with": {"label": "Starts With", "description": "String starts with value", "requires_value": True},
365
+ "ends_with": {"label": "Ends With", "description": "String ends with value", "requires_value": True},
366
+ "is_true": {"label": "Is True", "description": "Value is truthy", "requires_value": False},
367
+ "is_false": {"label": "Is False", "description": "Value is falsy", "requires_value": False},
368
+ }
369
+
370
+
371
+ def get_available_operators() -> Dict[str, Dict[str, Any]]:
372
+ """Get operator metadata for frontend UI."""
373
+ return OPERATORS.copy()
@@ -0,0 +1,132 @@
1
+ """Dead Letter Queue (DLQ) handler for failed node executions.
2
+
3
+ This module provides optional DLQ functionality that can be enabled/disabled
4
+ via configuration. When enabled, failed nodes (after all retries exhausted)
5
+ are stored for later inspection and replay.
6
+
7
+ Usage:
8
+ from services.execution.dlq import DLQHandler, NullDLQHandler
9
+
10
+ # Create handler based on config
11
+ dlq = DLQHandler(cache) if settings.dlq_enabled else NullDLQHandler()
12
+
13
+ # Add failed node
14
+ await dlq.add_failed_node(ctx, node, inputs, error)
15
+ """
16
+
17
+ from typing import Dict, Any, Protocol
18
+ from core.logging import get_logger
19
+ from .models import ExecutionContext, NodeExecution, DLQEntry
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ class DLQHandlerProtocol(Protocol):
25
+ """Protocol for DLQ handlers (enables duck typing)."""
26
+
27
+ async def add_failed_node(self, ctx: ExecutionContext, node: NodeExecution,
28
+ inputs: Dict[str, Any], error: str) -> bool:
29
+ """Add a failed node to the DLQ."""
30
+ ...
31
+
32
+ @property
33
+ def enabled(self) -> bool:
34
+ """Whether DLQ is enabled."""
35
+ ...
36
+
37
+
38
+ class NullDLQHandler:
39
+ """No-op DLQ handler when DLQ is disabled.
40
+
41
+ This follows the Null Object pattern - all operations succeed silently.
42
+ """
43
+
44
+ @property
45
+ def enabled(self) -> bool:
46
+ return False
47
+
48
+ async def add_failed_node(self, ctx: ExecutionContext, node: NodeExecution,
49
+ inputs: Dict[str, Any], error: str) -> bool:
50
+ """No-op: silently succeed without storing anything."""
51
+ logger.debug("DLQ disabled, skipping failed node storage",
52
+ node_id=node.node_id, error=error)
53
+ return True
54
+
55
+
56
+ class DLQHandler:
57
+ """Active DLQ handler that stores failed nodes in Redis.
58
+
59
+ Stores failed node executions with full context for later inspection
60
+ and replay via the replay_dlq_entry API.
61
+ """
62
+
63
+ def __init__(self, cache: "ExecutionCache"):
64
+ """Initialize DLQ handler.
65
+
66
+ Args:
67
+ cache: ExecutionCache instance for Redis persistence
68
+ """
69
+ self.cache = cache
70
+
71
+ @property
72
+ def enabled(self) -> bool:
73
+ return True
74
+
75
+ async def add_failed_node(self, ctx: ExecutionContext, node: NodeExecution,
76
+ inputs: Dict[str, Any], error: str) -> bool:
77
+ """Add a failed node to the Dead Letter Queue.
78
+
79
+ Args:
80
+ ctx: ExecutionContext with workflow info
81
+ node: Failed NodeExecution with retry info
82
+ inputs: Node inputs at time of failure
83
+ error: Final error message
84
+
85
+ Returns:
86
+ True if successfully added, False otherwise
87
+ """
88
+ try:
89
+ dlq_entry = DLQEntry.create(ctx, node, inputs)
90
+
91
+ success = await self.cache.add_to_dlq(dlq_entry)
92
+ if success:
93
+ logger.info("Node added to DLQ",
94
+ entry_id=dlq_entry.id,
95
+ node_id=node.node_id,
96
+ node_type=node.node_type,
97
+ retry_count=node.retry_count)
98
+
99
+ await self.cache.add_event(ctx.execution_id, "node_dlq", {
100
+ "node_id": node.node_id,
101
+ "dlq_entry_id": dlq_entry.id,
102
+ "error": error,
103
+ "retry_count": node.retry_count,
104
+ })
105
+ return True
106
+ else:
107
+ logger.error("Failed to add node to DLQ",
108
+ node_id=node.node_id, error=error)
109
+ return False
110
+
111
+ except Exception as e:
112
+ logger.error("Exception adding node to DLQ",
113
+ node_id=node.node_id, error=str(e))
114
+ return False
115
+
116
+
117
+ def create_dlq_handler(cache: "ExecutionCache", enabled: bool = False) -> DLQHandlerProtocol:
118
+ """Factory function to create appropriate DLQ handler.
119
+
120
+ Args:
121
+ cache: ExecutionCache instance
122
+ enabled: Whether DLQ should be enabled
123
+
124
+ Returns:
125
+ DLQHandler if enabled, NullDLQHandler otherwise
126
+ """
127
+ if enabled:
128
+ logger.info("DLQ enabled")
129
+ return DLQHandler(cache)
130
+ else:
131
+ logger.debug("DLQ disabled")
132
+ return NullDLQHandler()