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,23 @@
1
+ """Temporal workflow orchestration service.
2
+
3
+ This module provides Temporal integration for durable distributed workflow execution.
4
+
5
+ Architecture:
6
+ - Each workflow node executes as an independent Temporal activity
7
+ - Activities can run on ANY worker in the cluster for horizontal scaling
8
+ - Workflow only orchestrates - schedules activities and routes outputs
9
+ - WebSocket connection to MachinaOs for low-latency node execution
10
+
11
+ When TEMPORAL_ENABLED=true:
12
+ - Workflows are executed via Temporal for durability and distribution
13
+ - Each node is a separate activity with its own retry policy
14
+ - Parallel branches execute concurrently on available workers
15
+
16
+ When TEMPORAL_ENABLED=false (default):
17
+ - Falls back to the existing parallel/sequential executor
18
+ """
19
+
20
+ from .executor import TemporalExecutor
21
+ from .client import TemporalClientWrapper
22
+
23
+ __all__ = ["TemporalExecutor", "TemporalClientWrapper"]
@@ -0,0 +1,344 @@
1
+ """Temporal activities for distributed node execution.
2
+
3
+ Uses class-based activity pattern recommended by Temporal docs for sharing
4
+ resources like aiohttp.ClientSession across activity invocations.
5
+
6
+ References:
7
+ - https://docs.temporal.io/develop/python/python-sdk-sync-vs-async
8
+ - https://docs.temporal.io/develop/python/core-application
9
+
10
+ Architecture:
11
+ - NodeExecutionActivities class holds shared aiohttp.ClientSession
12
+ - Session is passed via constructor, avoiding recreation per activity
13
+ - Each activity call gets its own WebSocket connection from the session pool
14
+ """
15
+
16
+ from datetime import datetime
17
+ from typing import Any, Dict, Optional
18
+
19
+ import aiohttp
20
+ from temporalio import activity
21
+
22
+ from core.logging import get_logger
23
+ from core.config import Settings
24
+
25
+ logger = get_logger(__name__)
26
+
27
+ # Load settings to get the correct server port
28
+ _settings = Settings()
29
+ MACHINA_URL = f"http://{_settings.host}:{_settings.port}"
30
+ WS_URL = f"ws://{_settings.host}:{_settings.port}/ws/internal"
31
+
32
+ print(f"[Temporal Activities] MACHINA_URL configured: {MACHINA_URL}")
33
+ print(f"[Temporal Activities] WS_URL configured: {WS_URL}")
34
+
35
+
36
+ class NodeExecutionActivities:
37
+ """Activity class for node execution with shared aiohttp session.
38
+
39
+ Following Temporal's recommended pattern for dependency injection:
40
+ - aiohttp.ClientSession is passed via constructor
41
+ - Session provides connection pooling for concurrent activities
42
+ - Each activity call gets its own WebSocket connection from the pool
43
+
44
+ Reference: https://docs.temporal.io/develop/python/python-sdk-sync-vs-async
45
+ """
46
+
47
+ def __init__(self, session: aiohttp.ClientSession):
48
+ """Initialize with shared aiohttp session.
49
+
50
+ Args:
51
+ session: aiohttp.ClientSession with connection pooling configured
52
+ """
53
+ self.session = session
54
+ self.ws_url = WS_URL
55
+ self.http_url = f"{MACHINA_URL}/api/workflow/node/execute"
56
+ self.broadcast_url = f"{MACHINA_URL}/api/workflow/broadcast-status"
57
+
58
+ @activity.defn
59
+ async def execute_node_activity(self, context: Dict[str, Any]) -> Dict[str, Any]:
60
+ """Execute a single workflow node with isolated context.
61
+
62
+ This activity can run on ANY worker in the cluster, enabling
63
+ horizontal scaling and multi-tenant distribution.
64
+
65
+ Each node execution is independent - if it fails, Temporal will retry
66
+ on the same or different worker without affecting other nodes.
67
+
68
+ Args:
69
+ context: Immutable context containing:
70
+ - node_id: Unique node identifier
71
+ - node_type: Type of node (aiAgent, console, timer, etc.)
72
+ - node_data: Node configuration from React Flow
73
+ - inputs: Outputs from upstream nodes (dependencies)
74
+ - workflow_id: Parent workflow ID for tracking
75
+ - tenant_id: Tenant identifier for multi-tenancy
76
+ - session_id: Session identifier
77
+ - nodes: Full node list (for tool/memory detection by handlers)
78
+ - edges: Full edge list (for tool/memory detection by handlers)
79
+
80
+ Returns:
81
+ Dict with success, result, node_id, and metadata
82
+ """
83
+ node_id = context["node_id"]
84
+ node_type = context["node_type"]
85
+ node_data = context.get("node_data", {})
86
+ workflow_id = context.get("workflow_id")
87
+ tenant_id = context.get("tenant_id")
88
+
89
+ activity.logger.info(
90
+ f"Executing node activity: {node_id} ({node_type})",
91
+ extra={"tenant_id": tenant_id, "workflow_id": workflow_id},
92
+ )
93
+ print(f"[Activity] Starting node: {node_id} (type={node_type})")
94
+
95
+ # Heartbeat at start to signal activity is alive
96
+ activity.heartbeat(f"Starting {node_type}: {node_id}")
97
+
98
+ # Handle pre-executed trigger nodes (already have their output)
99
+ if context.get("pre_executed"):
100
+ print(f"[Activity] Node {node_id} is pre-executed, returning cached result")
101
+ result = {
102
+ "success": True,
103
+ "node_id": node_id,
104
+ "node_type": node_type,
105
+ "result": context.get("trigger_output", {}),
106
+ "pre_executed": True,
107
+ "timestamp": datetime.now().isoformat(),
108
+ }
109
+ await self._broadcast_status(node_id, "success", result, workflow_id)
110
+ return result
111
+
112
+ # Handle disabled nodes
113
+ if node_data.get("disabled"):
114
+ print(f"[Activity] Node {node_id} is disabled, skipping")
115
+ result = {
116
+ "success": True,
117
+ "node_id": node_id,
118
+ "node_type": node_type,
119
+ "skipped": True,
120
+ "reason": "disabled",
121
+ "timestamp": datetime.now().isoformat(),
122
+ }
123
+ await self._broadcast_status(node_id, "skipped", {"disabled": True}, workflow_id)
124
+ return result
125
+
126
+ # Broadcast "executing" status for UI updates
127
+ await self._broadcast_status(
128
+ node_id=node_id,
129
+ status="executing",
130
+ data={"node_type": node_type},
131
+ workflow_id=workflow_id,
132
+ )
133
+
134
+ try:
135
+ # Heartbeat before potentially long WebSocket operation
136
+ activity.heartbeat(f"Executing via WebSocket: {node_id}")
137
+
138
+ # Execute node via WebSocket (each call gets own connection from pool)
139
+ result = await self._execute_via_websocket(context)
140
+
141
+ # Add metadata
142
+ result["node_id"] = node_id
143
+ result["node_type"] = node_type
144
+ result["timestamp"] = datetime.now().isoformat()
145
+
146
+ # Broadcast result status
147
+ if result.get("success"):
148
+ await self._broadcast_status(
149
+ node_id=node_id,
150
+ status="success",
151
+ data={
152
+ "result": result.get("result"),
153
+ "execution_time": result.get("execution_time"),
154
+ },
155
+ workflow_id=workflow_id,
156
+ )
157
+ print(f"[Activity] Node {node_id} completed successfully")
158
+ else:
159
+ await self._broadcast_status(
160
+ node_id=node_id,
161
+ status="error",
162
+ data={"error": result.get("error")},
163
+ workflow_id=workflow_id,
164
+ )
165
+ print(f"[Activity] Node {node_id} failed: {result.get('error')}")
166
+
167
+ # Heartbeat for activity liveness
168
+ activity.heartbeat(f"Node {node_id} completed")
169
+
170
+ return result
171
+
172
+ except Exception as e:
173
+ error_msg = f"{type(e).__name__}: {str(e)}"
174
+ logger.error(f"Node {node_id} execution failed: {error_msg}")
175
+ print(f"[Activity] Node {node_id} EXCEPTION: {error_msg}")
176
+
177
+ # Broadcast error status
178
+ await self._broadcast_status(
179
+ node_id=node_id,
180
+ status="error",
181
+ data={"error": error_msg},
182
+ workflow_id=workflow_id,
183
+ )
184
+
185
+ # Raise to trigger Temporal retry mechanism
186
+ raise
187
+
188
+ async def _execute_via_websocket(self, context: Dict[str, Any]) -> Dict[str, Any]:
189
+ """Execute node via WebSocket using shared session's connection pool.
190
+
191
+ Each call creates a new WebSocket connection from the session's pool,
192
+ avoiding race conditions when multiple activities run concurrently.
193
+ """
194
+ import json
195
+ import uuid
196
+
197
+ node_id = context["node_id"]
198
+ node_type = context["node_type"]
199
+ request_id = str(uuid.uuid4())
200
+
201
+ message = {
202
+ "type": "execute_node",
203
+ "request_id": request_id,
204
+ "node_id": node_id,
205
+ "node_type": node_type,
206
+ "parameters": context.get("node_data", {}),
207
+ "nodes": context.get("nodes", []),
208
+ "edges": context.get("edges", []),
209
+ "session_id": context.get("session_id", "default"),
210
+ "workflow_id": context.get("workflow_id"),
211
+ }
212
+
213
+ print(f"[Activity] WebSocket execute for {node_id}")
214
+
215
+ try:
216
+ # Each activity gets its own WebSocket connection from the pool
217
+ async with self.session.ws_connect(
218
+ self.ws_url,
219
+ heartbeat=20,
220
+ receive_timeout=120,
221
+ ) as ws:
222
+ await ws.send_json(message)
223
+ print(f"[Activity] Sent request for {node_id}")
224
+
225
+ # Wait for response with matching request_id
226
+ async for msg in ws:
227
+ if msg.type == aiohttp.WSMsgType.TEXT:
228
+ response = json.loads(msg.data)
229
+ if response.get("request_id") == request_id:
230
+ print(f"[Activity] Got response for {node_id}: success={response.get('success')}")
231
+ return response
232
+ elif msg.type == aiohttp.WSMsgType.ERROR:
233
+ raise Exception(f"WebSocket error: {ws.exception()}")
234
+ elif msg.type == aiohttp.WSMsgType.CLOSED:
235
+ raise Exception("WebSocket closed unexpectedly")
236
+
237
+ raise Exception(f"No response for request {request_id}")
238
+
239
+ except aiohttp.ClientError as e:
240
+ raise Exception(f"WebSocket connection error: {e}")
241
+
242
+ async def _broadcast_status(
243
+ self,
244
+ node_id: str,
245
+ status: str,
246
+ data: dict,
247
+ workflow_id: str = None,
248
+ ) -> None:
249
+ """Broadcast node status for real-time UI updates.
250
+
251
+ Non-fatal - execution continues even if broadcast fails.
252
+ """
253
+ try:
254
+ async with self.session.post(
255
+ self.broadcast_url,
256
+ json={
257
+ "node_id": node_id,
258
+ "status": status,
259
+ "data": data or {},
260
+ "workflow_id": workflow_id,
261
+ },
262
+ timeout=aiohttp.ClientTimeout(total=5),
263
+ ) as response:
264
+ if response.status == 200:
265
+ print(f"[Activity] Broadcast: {node_id} -> {status}")
266
+ except Exception as e:
267
+ # Non-fatal - don't fail execution if broadcast fails
268
+ logger.warning(f"Broadcast failed for {node_id}: {e}")
269
+ print(f"[Activity] Broadcast failed (non-fatal): {e}")
270
+
271
+
272
+ # =============================================================================
273
+ # Factory function for creating activity instance with session
274
+ # =============================================================================
275
+
276
+ def create_node_activities(session: aiohttp.ClientSession) -> NodeExecutionActivities:
277
+ """Factory function to create activity instance with shared session.
278
+
279
+ This follows Temporal's recommended pattern for dependency injection.
280
+ The session should be created once when the worker starts and reused.
281
+
282
+ Args:
283
+ session: aiohttp.ClientSession with connection pooling
284
+
285
+ Returns:
286
+ NodeExecutionActivities instance ready for worker registration
287
+ """
288
+ return NodeExecutionActivities(session)
289
+
290
+
291
+ async def create_shared_session(pool_size: int = 100) -> aiohttp.ClientSession:
292
+ """Create a shared aiohttp session with connection pooling.
293
+
294
+ Args:
295
+ pool_size: Maximum number of concurrent connections
296
+
297
+ Returns:
298
+ Configured aiohttp.ClientSession
299
+ """
300
+ connector = aiohttp.TCPConnector(
301
+ limit=pool_size,
302
+ limit_per_host=pool_size,
303
+ enable_cleanup_closed=True,
304
+ )
305
+ timeout = aiohttp.ClientTimeout(
306
+ total=300, # 5 min total
307
+ connect=10, # 10 sec connect
308
+ )
309
+ session = aiohttp.ClientSession(
310
+ connector=connector,
311
+ timeout=timeout,
312
+ )
313
+ print(f"[Activities] Created shared session with pool_size={pool_size}")
314
+ return session
315
+
316
+
317
+ # =============================================================================
318
+ # Standalone activity function (for backwards compatibility)
319
+ # =============================================================================
320
+
321
+ # Global session for standalone function
322
+ _global_session: Optional[aiohttp.ClientSession] = None
323
+ _global_activities: Optional[NodeExecutionActivities] = None
324
+
325
+
326
+ async def _get_global_activities() -> NodeExecutionActivities:
327
+ """Get or create global activities instance."""
328
+ global _global_session, _global_activities
329
+
330
+ if _global_session is None or _global_session.closed:
331
+ _global_session = await create_shared_session()
332
+ _global_activities = NodeExecutionActivities(_global_session)
333
+
334
+ return _global_activities
335
+
336
+
337
+ @activity.defn
338
+ async def execute_node_activity(context: Dict[str, Any]) -> Dict[str, Any]:
339
+ """Standalone activity function for backwards compatibility.
340
+
341
+ For new code, use NodeExecutionActivities class with shared session.
342
+ """
343
+ activities = await _get_global_activities()
344
+ return await activities.execute_node_activity(context)
@@ -0,0 +1,76 @@
1
+ """Temporal client wrapper for MachinaOs.
2
+
3
+ Manages the Temporal client connection lifecycle.
4
+ """
5
+
6
+ from typing import Optional
7
+ from temporalio.client import Client
8
+ from temporalio.runtime import Runtime, TelemetryConfig
9
+
10
+ from core.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class TemporalClientWrapper:
16
+ """Wrapper around Temporal client for lifecycle management."""
17
+
18
+ def __init__(self, server_address: str, namespace: str = "default"):
19
+ """Initialize the client wrapper.
20
+
21
+ Args:
22
+ server_address: Temporal server address (e.g., "localhost:7233")
23
+ namespace: Temporal namespace to use
24
+ """
25
+ self.server_address = server_address
26
+ self.namespace = namespace
27
+ self._client: Optional[Client] = None
28
+ self._runtime: Optional[Runtime] = None
29
+
30
+ @property
31
+ def client(self) -> Optional[Client]:
32
+ """Get the underlying Temporal client."""
33
+ return self._client
34
+
35
+ @property
36
+ def is_connected(self) -> bool:
37
+ """Check if client is connected."""
38
+ return self._client is not None
39
+
40
+ async def connect(self) -> Client:
41
+ """Connect to the Temporal server.
42
+
43
+ Returns:
44
+ The connected Temporal client
45
+ """
46
+ if self._client is not None:
47
+ return self._client
48
+
49
+ logger.info(
50
+ "Connecting to Temporal server",
51
+ server_address=self.server_address,
52
+ namespace=self.namespace,
53
+ )
54
+
55
+ # Create runtime with worker heartbeating disabled to avoid warning on older servers
56
+ self._runtime = Runtime(
57
+ telemetry=TelemetryConfig(),
58
+ worker_heartbeat_interval=None,
59
+ )
60
+
61
+ self._client = await Client.connect(
62
+ self.server_address,
63
+ namespace=self.namespace,
64
+ runtime=self._runtime,
65
+ )
66
+
67
+ logger.info("Connected to Temporal server")
68
+ return self._client
69
+
70
+ async def disconnect(self) -> None:
71
+ """Disconnect from the Temporal server."""
72
+ if self._client is not None:
73
+ # Temporal client doesn't have an explicit close method,
74
+ # but we clear the reference
75
+ self._client = None
76
+ logger.info("Disconnected from Temporal server")
@@ -0,0 +1,147 @@
1
+ """Temporal executor for MachinaOs workflow execution.
2
+
3
+ Provides the same interface as WorkflowExecutor but delegates
4
+ execution to Temporal for durable workflow orchestration.
5
+ """
6
+
7
+ import time
8
+ import uuid
9
+ from datetime import datetime
10
+ from typing import Any, Dict, List, Optional, Callable
11
+
12
+ from temporalio.client import Client
13
+
14
+ from core.logging import get_logger
15
+ from .workflow import MachinaWorkflow
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ class TemporalExecutor:
21
+ """Workflow executor that uses Temporal for durable execution.
22
+
23
+ Provides a compatible interface with the existing WorkflowExecutor
24
+ so it can be used as a drop-in replacement when Temporal is enabled.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ client: Client,
30
+ task_queue: str = "machina-tasks",
31
+ status_callback: Optional[Callable] = None,
32
+ ):
33
+ """Initialize the Temporal executor.
34
+
35
+ Args:
36
+ client: Connected Temporal client
37
+ task_queue: Temporal task queue name
38
+ status_callback: Optional callback for node status updates
39
+ """
40
+ self.client = client
41
+ self.task_queue = task_queue
42
+ self.status_callback = status_callback
43
+
44
+ async def execute_workflow(
45
+ self,
46
+ workflow_id: str,
47
+ nodes: List[Dict],
48
+ edges: List[Dict],
49
+ session_id: str = "default",
50
+ enable_caching: bool = True,
51
+ ) -> Dict[str, Any]:
52
+ """Execute a workflow using Temporal.
53
+
54
+ Args:
55
+ workflow_id: Unique workflow identifier
56
+ nodes: List of node definitions
57
+ edges: List of edge definitions
58
+ session_id: Session identifier
59
+ enable_caching: Whether to enable result caching (passed to activity)
60
+
61
+ Returns:
62
+ Dict with success, outputs, execution_trace, and timing info
63
+ """
64
+ start_time = time.time()
65
+ execution_id = f"temporal-{uuid.uuid4().hex[:8]}"
66
+
67
+ logger.info(
68
+ "Starting Temporal workflow execution",
69
+ workflow_id=workflow_id,
70
+ execution_id=execution_id,
71
+ node_count=len(nodes),
72
+ edge_count=len(edges),
73
+ )
74
+
75
+ try:
76
+ # Execute workflow via Temporal
77
+ result = await self.client.execute_workflow(
78
+ MachinaWorkflow.run,
79
+ {
80
+ "nodes": nodes,
81
+ "edges": edges,
82
+ "session_id": session_id,
83
+ "workflow_id": workflow_id,
84
+ },
85
+ id=execution_id,
86
+ task_queue=self.task_queue,
87
+ )
88
+
89
+ execution_time = time.time() - start_time
90
+
91
+ # Notify status callback for completed nodes
92
+ if self.status_callback and result.get("success"):
93
+ for node_id in result.get("execution_trace", []):
94
+ try:
95
+ await self.status_callback(
96
+ node_id,
97
+ "completed",
98
+ result.get("outputs", {}).get(node_id, {}),
99
+ )
100
+ except Exception as e:
101
+ logger.warning(
102
+ f"Status callback error for node {node_id}: {e}"
103
+ )
104
+
105
+ logger.info(
106
+ "Temporal workflow completed",
107
+ workflow_id=workflow_id,
108
+ execution_id=execution_id,
109
+ success=result.get("success"),
110
+ nodes_executed=len(result.get("execution_trace", [])),
111
+ execution_time=execution_time,
112
+ )
113
+
114
+ return {
115
+ "success": result.get("success", False),
116
+ "execution_id": execution_id,
117
+ "nodes_executed": result.get("execution_trace", []),
118
+ "outputs": result.get("outputs", {}),
119
+ "errors": [result.get("error")] if result.get("error") else [],
120
+ "execution_time": execution_time,
121
+ "temporal_execution": True,
122
+ "timestamp": datetime.now().isoformat(),
123
+ }
124
+
125
+ except Exception as e:
126
+ import traceback
127
+ execution_time = time.time() - start_time
128
+ error_details = f"{type(e).__name__}: {str(e)}"
129
+ tb = traceback.format_exc()
130
+ logger.error(
131
+ f"Temporal workflow failed: {error_details}",
132
+ workflow_id=workflow_id,
133
+ execution_id=execution_id,
134
+ traceback=tb,
135
+ )
136
+ print(f"[Temporal] Workflow failed with exception:\n{tb}")
137
+
138
+ return {
139
+ "success": False,
140
+ "execution_id": execution_id,
141
+ "nodes_executed": [],
142
+ "outputs": {},
143
+ "errors": [error_details],
144
+ "execution_time": execution_time,
145
+ "temporal_execution": True,
146
+ "timestamp": datetime.now().isoformat(),
147
+ }