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,375 @@
1
+ """Node Executor - Single node execution with handler dispatch.
2
+
3
+ Uses a registry pattern for clean handler dispatch without if-else chains.
4
+ """
5
+
6
+ import asyncio
7
+ import time
8
+ import uuid
9
+ from dataclasses import dataclass
10
+ from datetime import datetime
11
+ from functools import partial
12
+ from typing import Dict, Any, Optional, Callable, TYPE_CHECKING
13
+
14
+ from core.logging import get_logger
15
+ from constants import (
16
+ ANDROID_SERVICE_NODE_TYPES,
17
+ AI_MODEL_TYPES,
18
+ AI_CHAT_MODEL_TYPES,
19
+ GOOGLE_MAPS_TYPES,
20
+ detect_ai_provider,
21
+ )
22
+ from models.nodes import validate_node_params
23
+ from pydantic import ValidationError
24
+ from services import event_waiter
25
+ from services.handlers import (
26
+ handle_ai_agent, handle_chat_agent, handle_ai_chat_model, handle_simple_memory,
27
+ handle_android_device_setup, handle_android_service,
28
+ handle_python_executor, handle_javascript_executor,
29
+ handle_http_request, handle_webhook_response, handle_trigger_node,
30
+ handle_create_map, handle_add_locations, handle_nearby_places,
31
+ handle_text_generator, handle_file_handler,
32
+ handle_chat_send, handle_chat_history,
33
+ handle_start, handle_cron_scheduler, handle_timer, handle_console,
34
+ handle_whatsapp_send, handle_whatsapp_connect, handle_whatsapp_db,
35
+ handle_http_scraper, handle_file_downloader, handle_document_parser,
36
+ handle_text_chunker, handle_embedding_generator, handle_vector_store,
37
+ )
38
+
39
+ if TYPE_CHECKING:
40
+ from core.config import Settings
41
+ from core.database import Database
42
+ from services.ai import AIService
43
+ from services.maps import MapsService
44
+ from services.text import TextService
45
+ from services.android_service import AndroidService
46
+
47
+ logger = get_logger(__name__)
48
+
49
+
50
+ @dataclass
51
+ class ExecutionResult:
52
+ """Standardized execution result."""
53
+ success: bool
54
+ node_id: str
55
+ node_type: str
56
+ result: Optional[Dict] = None
57
+ error: Optional[str] = None
58
+ execution_id: str = ""
59
+ execution_time: float = 0.0
60
+ timestamp: str = ""
61
+
62
+ def to_dict(self) -> Dict[str, Any]:
63
+ d = {
64
+ "success": self.success,
65
+ "node_id": self.node_id,
66
+ "node_type": self.node_type,
67
+ "execution_id": self.execution_id,
68
+ "execution_time": self.execution_time,
69
+ "timestamp": self.timestamp or datetime.now().isoformat(),
70
+ }
71
+ if self.success:
72
+ d["result"] = self.result or {}
73
+ else:
74
+ d["error"] = self.error
75
+ return d
76
+
77
+
78
+ class NodeExecutor:
79
+ """Executes individual workflow nodes using registry-based dispatch."""
80
+
81
+ def __init__(
82
+ self,
83
+ database: "Database",
84
+ ai_service: "AIService",
85
+ maps_service: "MapsService",
86
+ text_service: "TextService",
87
+ android_service: "AndroidService",
88
+ settings: "Settings",
89
+ output_store: Optional[Callable] = None,
90
+ ):
91
+ self.database = database
92
+ self.ai_service = ai_service
93
+ self.maps_service = maps_service
94
+ self.text_service = text_service
95
+ self.android_service = android_service
96
+ self.settings = settings
97
+ self._output_store = output_store
98
+ self._handlers = self._build_handler_registry()
99
+
100
+ def _build_handler_registry(self) -> Dict[str, Callable]:
101
+ """Build handler registry with service dependencies bound via partial."""
102
+ registry = {
103
+ # Workflow control
104
+ 'start': handle_start,
105
+ 'cronScheduler': handle_cron_scheduler,
106
+ 'timer': handle_timer,
107
+ # AI
108
+ 'aiAgent': partial(handle_ai_agent, ai_service=self.ai_service, database=self.database),
109
+ 'chatAgent': partial(handle_chat_agent, ai_service=self.ai_service, database=self.database),
110
+ 'simpleMemory': handle_simple_memory,
111
+ # Maps
112
+ 'createMap': partial(handle_create_map, maps_service=self.maps_service),
113
+ 'addLocations': partial(handle_add_locations, maps_service=self.maps_service),
114
+ 'showNearbyPlaces': partial(handle_nearby_places, maps_service=self.maps_service),
115
+ # Text
116
+ 'textGenerator': partial(handle_text_generator, text_service=self.text_service),
117
+ 'fileHandler': partial(handle_file_handler, text_service=self.text_service),
118
+ # WhatsApp
119
+ 'whatsappSend': handle_whatsapp_send,
120
+ 'whatsappConnect': handle_whatsapp_connect,
121
+ 'whatsappDb': handle_whatsapp_db,
122
+ # Chat
123
+ 'chatSend': handle_chat_send,
124
+ 'chatHistory': handle_chat_history,
125
+ # HTTP
126
+ 'httpRequest': handle_http_request,
127
+ # Android setup
128
+ 'androidDeviceSetup': partial(handle_android_device_setup, settings=self.settings),
129
+ # Document processing
130
+ 'httpScraper': handle_http_scraper,
131
+ 'fileDownloader': handle_file_downloader,
132
+ 'documentParser': handle_document_parser,
133
+ 'textChunker': handle_text_chunker,
134
+ 'embeddingGenerator': handle_embedding_generator,
135
+ 'vectorStore': handle_vector_store,
136
+ # Note: 'console' handled in _dispatch with connected_outputs
137
+ }
138
+
139
+ # Register AI chat models
140
+ for node_type in AI_CHAT_MODEL_TYPES:
141
+ registry[node_type] = partial(handle_ai_chat_model, ai_service=self.ai_service)
142
+
143
+ # Register Android services
144
+ for node_type in ANDROID_SERVICE_NODE_TYPES:
145
+ registry[node_type] = partial(handle_android_service, android_service=self.android_service)
146
+
147
+ return registry
148
+
149
+ async def execute(
150
+ self,
151
+ node_id: str,
152
+ node_type: str,
153
+ parameters: Dict[str, Any],
154
+ context: Dict[str, Any],
155
+ resolve_params_fn: Optional[Callable] = None,
156
+ ) -> Dict[str, Any]:
157
+ """Execute a single workflow node."""
158
+ start_time = time.time()
159
+ session_id = context.get('session_id', 'default')
160
+ execution_id = context.get('execution_id') or str(uuid.uuid4())[:8]
161
+
162
+ try:
163
+ # Load, validate, enhance parameters
164
+ params = await self._prepare_parameters(node_id, node_type, parameters, session_id)
165
+
166
+ # Resolve templates if resolver provided
167
+ nodes = context.get('nodes')
168
+ edges = context.get('edges')
169
+ logger.debug(f"[NodeExecutor] Template resolution check: resolve_fn={resolve_params_fn is not None}, nodes={len(nodes) if nodes else 'None'}, edges={len(edges) if edges else 'None'}")
170
+
171
+ if resolve_params_fn and nodes is not None and edges is not None:
172
+ logger.debug(f"[NodeExecutor] Before resolution: params={list(params.keys())}")
173
+ params = await resolve_params_fn(params, node_id, nodes, edges, session_id)
174
+ logger.debug(f"[NodeExecutor] After resolution: params keys={list(params.keys())}")
175
+
176
+ # Build handler context
177
+ handler_ctx = {
178
+ **context,
179
+ "start_time": start_time,
180
+ "execution_id": execution_id,
181
+ }
182
+ logger.info("NodeExecutor context", node_id=node_id, workflow_id=context.get('workflow_id'))
183
+
184
+ # Execute via registry or special handlers
185
+ result = await self._dispatch(node_id, node_type, params, handler_ctx)
186
+ result['execution_id'] = execution_id
187
+
188
+ # Store output if successful
189
+ if result.get('success') and self._output_store:
190
+ output_data = result.get('result', {})
191
+
192
+ # For Android service nodes, extract the nested 'data' field for cleaner template access
193
+ # This allows {{batterymonitor.battery_level}} instead of {{batterymonitor.data.battery_level}}
194
+ if node_type in ANDROID_SERVICE_NODE_TYPES and isinstance(output_data, dict):
195
+ # Flatten: promote 'data' contents to top level while preserving metadata
196
+ nested_data = output_data.get('data', {})
197
+ if isinstance(nested_data, dict):
198
+ # Merge nested data with metadata (service_id, action, timestamp, etc.)
199
+ output_data = {**output_data, **nested_data}
200
+ logger.debug(f"[NodeExecutor] Flattened Android output for {node_id}: keys={list(output_data.keys())}")
201
+
202
+ await self._output_store(session_id, node_id, "output_0", output_data)
203
+
204
+ return result
205
+
206
+ except asyncio.CancelledError:
207
+ return ExecutionResult(False, node_id, node_type, error="Cancelled",
208
+ execution_id=execution_id, execution_time=time.time()-start_time).to_dict()
209
+ except Exception as e:
210
+ logger.error("Node execution error", node_id=node_id, error=str(e))
211
+ return ExecutionResult(False, node_id, node_type, error=str(e),
212
+ execution_id=execution_id, execution_time=time.time()-start_time).to_dict()
213
+
214
+ async def _prepare_parameters(self, node_id: str, node_type: str, params: Dict, session_id: str) -> Dict:
215
+ """Load from DB, validate, inject API keys."""
216
+ # Merge with DB parameters (DB provides defaults, frontend can override)
217
+ db_params = await self.database.get_node_parameters(node_id) or {}
218
+ merged = {**db_params, **params} if params else db_params
219
+
220
+ # Validate
221
+ try:
222
+ validated = validate_node_params(node_type, merged)
223
+ merged = {**merged, **validated.model_dump(by_alias=True, exclude_unset=True)}
224
+ except ValidationError as e:
225
+ logger.warning("Validation warning", node_type=node_type, errors=str(e))
226
+
227
+ # Inject API keys
228
+ return await self._inject_api_keys(node_type, merged)
229
+
230
+ async def _inject_api_keys(self, node_type: str, params: Dict) -> Dict:
231
+ """Auto-inject API keys for AI and Maps nodes."""
232
+ result = params.copy()
233
+
234
+ if node_type in AI_MODEL_TYPES:
235
+ provider = detect_ai_provider(node_type, params)
236
+ if not result.get('api_key') and not result.get('apiKey'):
237
+ key = await self.ai_service.auth.get_api_key(provider, "default")
238
+ if key:
239
+ result['api_key'] = key
240
+ if not result.get('model'):
241
+ models = await self.ai_service.auth.get_stored_models(provider, "default")
242
+ if models:
243
+ result['model'] = models[0]
244
+
245
+ elif node_type in GOOGLE_MAPS_TYPES:
246
+ if not result.get('api_key'):
247
+ # Try database first, then fall back to environment variable
248
+ key = await self.ai_service.auth.get_api_key("google_maps", "default")
249
+ if key:
250
+ result['api_key'] = key
251
+ elif self.settings.google_maps_api_key:
252
+ result['api_key'] = self.settings.google_maps_api_key
253
+
254
+ return result
255
+
256
+ async def _dispatch(self, node_id: str, node_type: str, params: Dict, context: Dict) -> Dict:
257
+ """Dispatch to handler from registry or special handlers."""
258
+
259
+ # Check registry first
260
+ handler = self._handlers.get(node_type)
261
+ if handler:
262
+ return await handler(node_id, node_type, params, context)
263
+
264
+ # Special handlers needing connected outputs
265
+ if node_type in ('pythonExecutor', 'javascriptExecutor', 'webhookResponse', 'console'):
266
+ outputs, source_nodes = await self._get_connected_outputs_with_info(context, node_id)
267
+ if node_type == 'console':
268
+ return await handle_console(node_id, node_type, params, context, outputs, source_nodes)
269
+ handlers = {
270
+ 'pythonExecutor': handle_python_executor,
271
+ 'javascriptExecutor': handle_javascript_executor,
272
+ 'webhookResponse': handle_webhook_response,
273
+ }
274
+ return await handlers[node_type](node_id, node_type, params, context, outputs)
275
+
276
+ # Trigger nodes
277
+ if event_waiter.is_trigger_node(node_type):
278
+ return await handle_trigger_node(node_id, node_type, params, context)
279
+
280
+ # Fallback
281
+ return {
282
+ "success": True,
283
+ "node_id": node_id,
284
+ "node_type": node_type,
285
+ "result": {"message": f"Node {node_id} executed", "parameters": params},
286
+ "execution_time": time.time() - context.get('start_time', time.time()),
287
+ "timestamp": datetime.now().isoformat()
288
+ }
289
+
290
+ async def _get_connected_outputs(self, context: Dict, node_id: str) -> Dict[str, Any]:
291
+ """Get outputs from connected upstream nodes."""
292
+ get_output = context.get('get_output_fn')
293
+ if not get_output:
294
+ return {}
295
+
296
+ nodes = context.get('nodes', [])
297
+ edges = context.get('edges', [])
298
+ session_id = context.get('session_id', 'default')
299
+ result = {}
300
+
301
+ for edge in edges:
302
+ if edge.get('target') == node_id:
303
+ source_id = edge.get('source')
304
+ output = await get_output(session_id, source_id, "output_0")
305
+ if output:
306
+ source = next((n for n in nodes if n.get('id') == source_id), {})
307
+ result[source.get('type', 'unknown')] = output
308
+
309
+ return result
310
+
311
+ def _get_source_nodes_info(self, context: Dict, node_id: str) -> list:
312
+ """Get source node info (id, type, label) for edges targeting this node.
313
+
314
+ This is used for display purposes (e.g., showing source in Console panel).
315
+ Does NOT filter by output availability - just returns edge source info.
316
+ """
317
+ nodes = context.get('nodes', [])
318
+ edges = context.get('edges', [])
319
+ source_nodes = []
320
+
321
+ for edge in edges:
322
+ if edge.get('target') == node_id:
323
+ source_id = edge.get('source')
324
+ source = next((n for n in nodes if n.get('id') == source_id), {})
325
+ source_type = source.get('type', 'unknown')
326
+ source_data = source.get('data', {})
327
+ source_label = source_data.get('label') or source_type
328
+ source_nodes.append({
329
+ 'id': source_id,
330
+ 'type': source_type,
331
+ 'label': source_label
332
+ })
333
+
334
+ return source_nodes
335
+
336
+ async def _get_connected_outputs_with_info(self, context: Dict, node_id: str) -> tuple:
337
+ """Get outputs from connected upstream nodes with source node info.
338
+
339
+ Returns:
340
+ Tuple of (outputs dict, source_nodes list with id/type/label info)
341
+ """
342
+ get_output = context.get('get_output_fn')
343
+ if not get_output:
344
+ logger.warning(f"[_get_connected_outputs_with_info] No get_output_fn in context for {node_id}")
345
+ return {}, []
346
+
347
+ nodes = context.get('nodes', [])
348
+ edges = context.get('edges', [])
349
+ session_id = context.get('session_id', 'default')
350
+ outputs = {}
351
+ source_nodes = []
352
+
353
+ logger.debug(f"[_get_connected_outputs_with_info] node_id={node_id}, edges={len(edges)}, session={session_id}")
354
+
355
+ for edge in edges:
356
+ if edge.get('target') == node_id:
357
+ source_id = edge.get('source')
358
+ logger.debug(f"[_get_connected_outputs_with_info] Found edge from {source_id} to {node_id}")
359
+ output = await get_output(session_id, source_id, "output_0")
360
+ logger.debug(f"[_get_connected_outputs_with_info] Output from {source_id}: {'FOUND' if output else 'NOT FOUND'}")
361
+ if output:
362
+ source = next((n for n in nodes if n.get('id') == source_id), {})
363
+ source_type = source.get('type', 'unknown')
364
+ outputs[source_type] = output
365
+ # Get label from node data if available
366
+ source_data = source.get('data', {})
367
+ source_label = source_data.get('label') or source_type
368
+ source_nodes.append({
369
+ 'id': source_id,
370
+ 'type': source_type,
371
+ 'label': source_label
372
+ })
373
+
374
+ logger.debug(f"[_get_connected_outputs_with_info] Returning {len(outputs)} outputs, {len(source_nodes)} source_nodes")
375
+ return outputs, source_nodes
@@ -0,0 +1,218 @@
1
+ """Parameter Resolver - Template variable resolution.
2
+
3
+ Resolves {{node.field}} template variables in parameters using connected node outputs.
4
+ """
5
+
6
+ import re
7
+ from typing import Dict, Any, List, Optional, Callable, TYPE_CHECKING
8
+
9
+ from core.logging import get_logger
10
+
11
+ if TYPE_CHECKING:
12
+ from core.database import Database
13
+
14
+ logger = get_logger(__name__)
15
+
16
+ # Compiled regex for template matching
17
+ TEMPLATE_PATTERN = re.compile(r'\{\{([^}]+)\}\}')
18
+
19
+
20
+ class ParameterResolver:
21
+ """Resolves template variables in node parameters."""
22
+
23
+ def __init__(self, database: "Database", get_output_fn: Callable):
24
+ """
25
+ Args:
26
+ database: Database for loading node parameters
27
+ get_output_fn: Async function to get node output
28
+ Signature: async def (session_id, node_id, output_name) -> Dict
29
+ """
30
+ self.database = database
31
+ self.get_output = get_output_fn
32
+
33
+ async def resolve(
34
+ self,
35
+ parameters: Dict[str, Any],
36
+ node_id: str,
37
+ nodes: List[Dict],
38
+ edges: List[Dict],
39
+ session_id: str
40
+ ) -> Dict[str, Any]:
41
+ """Resolve all template variables in parameters."""
42
+ # Build connected data map from upstream nodes
43
+ connected_data = await self._gather_connected_outputs(node_id, nodes, edges, session_id)
44
+
45
+ # Resolve templates
46
+ return self._resolve_templates(parameters, connected_data)
47
+
48
+ async def _gather_connected_outputs(
49
+ self,
50
+ node_id: str,
51
+ nodes: List[Dict],
52
+ edges: List[Dict],
53
+ session_id: str
54
+ ) -> Dict[str, Any]:
55
+ """Gather outputs from all nodes in the workflow that have executed.
56
+
57
+ n8n pattern: Template variables can reference ANY node's output in the workflow,
58
+ not just directly connected nodes. This allows flexible data flow patterns like:
59
+ - A -> B -> C where C references A's output directly
60
+ - Parallel branches where downstream nodes reference any upstream node
61
+ """
62
+ connected = {}
63
+
64
+ logger.debug(f"[ParameterResolver] Gathering outputs for node {node_id}, session_id={session_id}, total nodes: {len(nodes)}")
65
+
66
+ # Gather outputs from ALL nodes (not just directly connected)
67
+ # This allows {{nodeName.field}} to reference any previously executed node
68
+ for source_node in nodes:
69
+ source_id = source_node.get('id')
70
+ if source_id == node_id:
71
+ continue # Skip self
72
+
73
+ node_type = source_node.get('type', '')
74
+ node_label = source_node.get('data', {}).get('label', 'NO_LABEL')
75
+ node_key = self._get_template_key(source_node)
76
+
77
+ logger.debug(f"[ParameterResolver] Processing node: id={source_id}, type={node_type}, label={node_label}, key={node_key}")
78
+
79
+ # Special handling for start nodes
80
+ if node_type == 'start':
81
+ data = await self._get_start_node_data(source_id)
82
+ else:
83
+ data = await self.get_output(session_id, source_id, "output_0")
84
+ logger.debug(f"[ParameterResolver] Output lookup: session={session_id}, node={source_id}, result={'FOUND' if data else 'NOT_FOUND'}")
85
+
86
+ if data:
87
+ connected[node_key] = data
88
+ logger.debug(f"[ParameterResolver] Stored output for key '{node_key}' (type={node_type}): keys={list(data.keys()) if isinstance(data, dict) else type(data)}")
89
+
90
+ logger.debug(f"[ParameterResolver] Available data keys for resolution: {list(connected.keys())}")
91
+ return connected
92
+
93
+ async def _get_start_node_data(self, node_id: str) -> Optional[Dict]:
94
+ """Get initial data from start node parameters."""
95
+ import json
96
+ params = await self.database.get_node_parameters(node_id)
97
+ if not params or 'initialData' not in params:
98
+ return {}
99
+
100
+ initial_data = params.get('initialData', '{}')
101
+ try:
102
+ return json.loads(initial_data) if isinstance(initial_data, str) else initial_data
103
+ except Exception:
104
+ return {}
105
+
106
+ def _get_template_key(self, node: Dict) -> str:
107
+ """Get template key for a node (lowercase, no spaces).
108
+
109
+ Priority matches frontend useDragVariable hook:
110
+ 1. node.data.label (user-defined label)
111
+ 2. node.data.displayName (from node definition)
112
+ 3. node.type (lowercased)
113
+ 4. node.id (fallback)
114
+ """
115
+ # Priority 1: User-defined label
116
+ label = node.get('data', {}).get('label')
117
+ if label:
118
+ return re.sub(r'\s+', '', label.lower())
119
+
120
+ # Priority 2: displayName from node definition (passed in node.data)
121
+ display_name = node.get('data', {}).get('displayName')
122
+ if display_name:
123
+ return re.sub(r'\s+', '', display_name.lower())
124
+
125
+ # Priority 3: node type
126
+ node_type = node.get('type', '')
127
+ if node_type:
128
+ return node_type.lower()
129
+
130
+ # Priority 4: node id
131
+ return node.get('id', 'unknown').lower()
132
+
133
+ def _resolve_templates(self, parameters: Dict[str, Any], connected_data: Dict[str, Any]) -> Dict[str, Any]:
134
+ """Resolve {{variable}} templates in parameters recursively."""
135
+ # Case-insensitive lookup
136
+ data_lower = {k.lower(): v for k, v in connected_data.items()}
137
+
138
+ # Log template resolution context at debug level
139
+ if logger.isEnabledFor(10): # DEBUG level
140
+ template_params = {k: v for k, v in parameters.items() if isinstance(v, str) and '{{' in v}
141
+ if template_params:
142
+ logger.debug(f"[ParameterResolver] Resolving templates: {list(template_params.keys())}")
143
+
144
+ def resolve(value: Any) -> Any:
145
+ if isinstance(value, str) and '{{' in value:
146
+ return self._resolve_string(value, data_lower)
147
+ if isinstance(value, dict):
148
+ return {k: resolve(v) for k, v in value.items()}
149
+ if isinstance(value, list):
150
+ return [resolve(item) for item in value]
151
+ return value
152
+
153
+ return {k: resolve(v) for k, v in parameters.items()}
154
+
155
+ def _resolve_string(self, value: str, data: Dict[str, Any]) -> Any:
156
+ """Resolve templates in a string value."""
157
+ result = value
158
+
159
+ for match in TEMPLATE_PATTERN.finditer(value):
160
+ full_match = match.group(0)
161
+ path = match.group(1).split('.')
162
+ node_name = path[0].lower()
163
+ property_path = path[1:]
164
+
165
+ node_data = data.get(node_name)
166
+ resolved_value = self._navigate_path(node_data, property_path)
167
+
168
+ logger.debug(f"[ParameterResolver] Resolving '{full_match}': node_name={node_name}, path={property_path}, found_data={node_data is not None}, resolved={resolved_value is not None}")
169
+
170
+ if resolved_value is not None:
171
+ # If entire value is just the template, preserve type
172
+ if value.strip() == full_match:
173
+ return resolved_value
174
+ result = result.replace(full_match, str(resolved_value))
175
+ else:
176
+ # Log missing resolution for debugging
177
+ logger.debug(f"[ParameterResolver] Could not resolve '{full_match}': available keys={list(data.keys())}")
178
+ result = result.replace(full_match, '')
179
+
180
+ return result
181
+
182
+ def _navigate_path(self, data: Any, path: List[str]) -> Any:
183
+ """Navigate through nested dict/list using path parts.
184
+
185
+ Supports:
186
+ - Dict keys: 'field' -> data['field']
187
+ - Array indexing: 'items[0]' -> data['items'][0]
188
+ - Nested paths: 'messages[0].text' -> data['messages'][0]['text']
189
+ """
190
+ current = data
191
+ for part in path:
192
+ if current is None:
193
+ return None
194
+
195
+ # Check for array index notation: field[index]
196
+ bracket_match = re.match(r'^(\w+)\[(\d+)\]$', part)
197
+ if bracket_match:
198
+ field_name = bracket_match.group(1)
199
+ index = int(bracket_match.group(2))
200
+
201
+ # Navigate to the field first
202
+ if isinstance(current, dict) and field_name in current:
203
+ current = current[field_name]
204
+ else:
205
+ return None
206
+
207
+ # Then access the array index
208
+ if isinstance(current, list) and 0 <= index < len(current):
209
+ current = current[index]
210
+ else:
211
+ return None
212
+ else:
213
+ # Standard dict key navigation
214
+ if not isinstance(current, dict) or part not in current:
215
+ return None
216
+ current = current[part]
217
+
218
+ return current