machinaos 0.0.1 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/.env.template +71 -71
  2. package/LICENSE +21 -21
  3. package/README.md +163 -87
  4. package/bin/cli.js +62 -106
  5. package/client/.dockerignore +45 -45
  6. package/client/Dockerfile +68 -68
  7. package/client/dist/assets/index-DFSC53FP.css +1 -0
  8. package/client/dist/assets/index-fJ-1gTf5.js +613 -0
  9. package/client/dist/index.html +14 -0
  10. package/client/eslint.config.js +34 -16
  11. package/client/nginx.conf +66 -66
  12. package/client/package.json +61 -48
  13. package/client/src/App.tsx +27 -27
  14. package/client/src/Dashboard.tsx +1200 -1172
  15. package/client/src/ParameterPanel.tsx +302 -300
  16. package/client/src/components/AIAgentNode.tsx +315 -321
  17. package/client/src/components/APIKeyValidator.tsx +117 -117
  18. package/client/src/components/ClaudeChatModelNode.tsx +17 -17
  19. package/client/src/components/CredentialsModal.tsx +1200 -306
  20. package/client/src/components/GeminiChatModelNode.tsx +17 -17
  21. package/client/src/components/GenericNode.tsx +356 -356
  22. package/client/src/components/LocationParameterPanel.tsx +153 -153
  23. package/client/src/components/ModelNode.tsx +285 -285
  24. package/client/src/components/OpenAIChatModelNode.tsx +17 -17
  25. package/client/src/components/OutputPanel.tsx +470 -470
  26. package/client/src/components/ParameterRenderer.tsx +1873 -1873
  27. package/client/src/components/SkillEditorModal.tsx +3 -3
  28. package/client/src/components/SquareNode.tsx +812 -796
  29. package/client/src/components/ToolkitNode.tsx +365 -365
  30. package/client/src/components/auth/LoginPage.tsx +247 -247
  31. package/client/src/components/auth/ProtectedRoute.tsx +59 -59
  32. package/client/src/components/base/BaseChatModelNode.tsx +270 -270
  33. package/client/src/components/icons/AIProviderIcons.tsx +50 -50
  34. package/client/src/components/maps/GoogleMapsPicker.tsx +136 -136
  35. package/client/src/components/maps/MapsPreviewPanel.tsx +109 -109
  36. package/client/src/components/maps/index.ts +25 -25
  37. package/client/src/components/parameterPanel/InputSection.tsx +1094 -1094
  38. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +64 -64
  39. package/client/src/components/parameterPanel/MapsSection.tsx +91 -91
  40. package/client/src/components/parameterPanel/MiddleSection.tsx +867 -571
  41. package/client/src/components/parameterPanel/OutputSection.tsx +80 -80
  42. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +81 -81
  43. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -436
  44. package/client/src/components/parameterPanel/index.ts +41 -41
  45. package/client/src/components/shared/DataPanel.tsx +142 -142
  46. package/client/src/components/shared/JSONTreeRenderer.tsx +105 -105
  47. package/client/src/components/ui/AIResultModal.tsx +203 -203
  48. package/client/src/components/ui/ApiKeyInput.tsx +93 -0
  49. package/client/src/components/ui/CodeEditor.tsx +81 -81
  50. package/client/src/components/ui/CollapsibleSection.tsx +87 -87
  51. package/client/src/components/ui/ComponentItem.tsx +153 -153
  52. package/client/src/components/ui/ComponentPalette.tsx +320 -320
  53. package/client/src/components/ui/ConsolePanel.tsx +151 -43
  54. package/client/src/components/ui/ErrorBoundary.tsx +195 -195
  55. package/client/src/components/ui/InputNodesPanel.tsx +203 -203
  56. package/client/src/components/ui/MapSelector.tsx +313 -313
  57. package/client/src/components/ui/Modal.tsx +151 -148
  58. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -1150
  59. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -381
  60. package/client/src/components/ui/QRCodeDisplay.tsx +182 -0
  61. package/client/src/components/ui/TopToolbar.tsx +736 -736
  62. package/client/src/components/ui/WorkflowSidebar.tsx +293 -293
  63. package/client/src/config/antdTheme.ts +186 -186
  64. package/client/src/contexts/AuthContext.tsx +221 -221
  65. package/client/src/contexts/ThemeContext.tsx +42 -42
  66. package/client/src/contexts/WebSocketContext.tsx +2144 -1971
  67. package/client/src/factories/baseChatModelFactory.ts +255 -255
  68. package/client/src/hooks/useAndroidOperations.ts +118 -164
  69. package/client/src/hooks/useApiKeyValidation.ts +106 -106
  70. package/client/src/hooks/useApiKeys.ts +238 -238
  71. package/client/src/hooks/useAppTheme.ts +17 -17
  72. package/client/src/hooks/useComponentPalette.ts +50 -50
  73. package/client/src/hooks/useDragAndDrop.ts +123 -123
  74. package/client/src/hooks/useDragVariable.ts +88 -88
  75. package/client/src/hooks/useExecution.ts +319 -313
  76. package/client/src/hooks/useParameterPanel.ts +176 -176
  77. package/client/src/hooks/useReactFlowNodes.ts +188 -188
  78. package/client/src/hooks/useToolSchema.ts +209 -209
  79. package/client/src/hooks/useWhatsApp.ts +196 -196
  80. package/client/src/hooks/useWorkflowManagement.ts +45 -45
  81. package/client/src/index.css +314 -314
  82. package/client/src/nodeDefinitions/aiAgentNodes.ts +335 -335
  83. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -340
  84. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -383
  85. package/client/src/nodeDefinitions/chatNodes.ts +135 -135
  86. package/client/src/nodeDefinitions/codeNodes.ts +54 -54
  87. package/client/src/nodeDefinitions/index.ts +14 -14
  88. package/client/src/nodeDefinitions/locationNodes.ts +462 -462
  89. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -220
  90. package/client/src/nodeDefinitions/skillNodes.ts +17 -5
  91. package/client/src/nodeDefinitions/utilityNodes.ts +284 -284
  92. package/client/src/nodeDefinitions/whatsappNodes.ts +821 -865
  93. package/client/src/nodeDefinitions.ts +101 -103
  94. package/client/src/services/dynamicParameterService.ts +95 -95
  95. package/client/src/services/execution/aiAgentExecutionService.ts +34 -34
  96. package/client/src/services/executionService.ts +227 -231
  97. package/client/src/services/workflowApi.ts +91 -91
  98. package/client/src/store/useAppStore.ts +578 -581
  99. package/client/src/styles/theme.ts +513 -508
  100. package/client/src/styles/zIndex.ts +16 -16
  101. package/client/src/types/ComponentTypes.ts +38 -38
  102. package/client/src/types/INodeProperties.ts +287 -287
  103. package/client/src/types/NodeTypes.ts +27 -27
  104. package/client/src/utils/formatters.ts +32 -32
  105. package/client/src/utils/googleMapsLoader.ts +139 -139
  106. package/client/src/utils/locationUtils.ts +84 -84
  107. package/client/src/utils/nodeUtils.ts +30 -30
  108. package/client/src/utils/workflow.ts +29 -29
  109. package/client/src/vite-env.d.ts +12 -12
  110. package/client/tailwind.config.js +59 -59
  111. package/client/tsconfig.json +25 -25
  112. package/client/vite.config.js +35 -35
  113. package/install.ps1 +308 -0
  114. package/install.sh +343 -0
  115. package/package.json +81 -70
  116. package/scripts/build.js +174 -51
  117. package/scripts/clean.js +40 -40
  118. package/scripts/start.js +234 -210
  119. package/scripts/stop.js +301 -325
  120. package/server/.dockerignore +44 -44
  121. package/server/Dockerfile +45 -45
  122. package/server/constants.py +244 -249
  123. package/server/core/cache.py +460 -460
  124. package/server/core/config.py +127 -127
  125. package/server/core/container.py +98 -98
  126. package/server/core/database.py +1296 -1210
  127. package/server/core/logging.py +313 -313
  128. package/server/main.py +288 -288
  129. package/server/middleware/__init__.py +5 -5
  130. package/server/middleware/auth.py +89 -89
  131. package/server/models/auth.py +52 -52
  132. package/server/models/cache.py +24 -24
  133. package/server/models/database.py +235 -210
  134. package/server/models/nodes.py +435 -455
  135. package/server/pyproject.toml +75 -72
  136. package/server/requirements.txt +83 -83
  137. package/server/routers/android.py +294 -294
  138. package/server/routers/auth.py +203 -203
  139. package/server/routers/database.py +150 -150
  140. package/server/routers/maps.py +141 -141
  141. package/server/routers/nodejs_compat.py +288 -288
  142. package/server/routers/webhook.py +90 -90
  143. package/server/routers/websocket.py +2239 -2127
  144. package/server/routers/whatsapp.py +761 -761
  145. package/server/routers/workflow.py +199 -199
  146. package/server/services/ai.py +2444 -2414
  147. package/server/services/android_service.py +588 -588
  148. package/server/services/auth.py +130 -130
  149. package/server/services/chat_client.py +160 -160
  150. package/server/services/deployment/manager.py +706 -706
  151. package/server/services/event_waiter.py +675 -785
  152. package/server/services/execution/executor.py +1351 -1351
  153. package/server/services/execution/models.py +1 -1
  154. package/server/services/handlers/__init__.py +122 -126
  155. package/server/services/handlers/ai.py +390 -355
  156. package/server/services/handlers/android.py +69 -260
  157. package/server/services/handlers/code.py +278 -278
  158. package/server/services/handlers/http.py +193 -193
  159. package/server/services/handlers/tools.py +146 -32
  160. package/server/services/handlers/triggers.py +107 -107
  161. package/server/services/handlers/utility.py +822 -822
  162. package/server/services/handlers/whatsapp.py +423 -476
  163. package/server/services/maps.py +288 -288
  164. package/server/services/memory_store.py +103 -103
  165. package/server/services/node_executor.py +372 -375
  166. package/server/services/scheduler.py +155 -155
  167. package/server/services/skill_loader.py +1 -1
  168. package/server/services/status_broadcaster.py +834 -826
  169. package/server/services/temporal/__init__.py +23 -23
  170. package/server/services/temporal/activities.py +344 -344
  171. package/server/services/temporal/client.py +76 -76
  172. package/server/services/temporal/executor.py +147 -147
  173. package/server/services/temporal/worker.py +251 -251
  174. package/server/services/temporal/workflow.py +355 -355
  175. package/server/services/temporal/ws_client.py +236 -236
  176. package/server/services/text.py +110 -110
  177. package/server/services/user_auth.py +172 -172
  178. package/server/services/websocket_client.py +29 -29
  179. package/server/services/workflow.py +597 -597
  180. package/server/skills/android-skill/SKILL.md +4 -4
  181. package/server/skills/code-skill/SKILL.md +123 -89
  182. package/server/skills/maps-skill/SKILL.md +3 -3
  183. package/server/skills/memory-skill/SKILL.md +1 -1
  184. package/server/skills/web-search-skill/SKILL.md +154 -0
  185. package/server/skills/whatsapp-skill/SKILL.md +3 -3
  186. package/server/uv.lock +461 -100
  187. package/server/whatsapp-rpc/.dockerignore +30 -30
  188. package/server/whatsapp-rpc/Dockerfile +44 -44
  189. package/server/whatsapp-rpc/Dockerfile.web +17 -17
  190. package/server/whatsapp-rpc/README.md +139 -139
  191. package/server/whatsapp-rpc/bin/whatsapp-rpc-server +0 -0
  192. package/server/whatsapp-rpc/cli.js +95 -95
  193. package/server/whatsapp-rpc/configs/config.yaml +6 -6
  194. package/server/whatsapp-rpc/docker-compose.yml +35 -35
  195. package/server/whatsapp-rpc/docs/API.md +410 -410
  196. package/server/whatsapp-rpc/node_modules/.package-lock.json +259 -0
  197. package/server/whatsapp-rpc/node_modules/chalk/license +9 -0
  198. package/server/whatsapp-rpc/node_modules/chalk/package.json +83 -0
  199. package/server/whatsapp-rpc/node_modules/chalk/readme.md +297 -0
  200. package/server/whatsapp-rpc/node_modules/chalk/source/index.d.ts +325 -0
  201. package/server/whatsapp-rpc/node_modules/chalk/source/index.js +225 -0
  202. package/server/whatsapp-rpc/node_modules/chalk/source/utilities.js +33 -0
  203. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  204. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  205. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  206. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  207. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  208. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  209. package/server/whatsapp-rpc/node_modules/commander/LICENSE +22 -0
  210. package/server/whatsapp-rpc/node_modules/commander/Readme.md +1148 -0
  211. package/server/whatsapp-rpc/node_modules/commander/esm.mjs +16 -0
  212. package/server/whatsapp-rpc/node_modules/commander/index.js +26 -0
  213. package/server/whatsapp-rpc/node_modules/commander/lib/argument.js +145 -0
  214. package/server/whatsapp-rpc/node_modules/commander/lib/command.js +2179 -0
  215. package/server/whatsapp-rpc/node_modules/commander/lib/error.js +43 -0
  216. package/server/whatsapp-rpc/node_modules/commander/lib/help.js +462 -0
  217. package/server/whatsapp-rpc/node_modules/commander/lib/option.js +329 -0
  218. package/server/whatsapp-rpc/node_modules/commander/lib/suggestSimilar.js +100 -0
  219. package/server/whatsapp-rpc/node_modules/commander/package-support.json +16 -0
  220. package/server/whatsapp-rpc/node_modules/commander/package.json +80 -0
  221. package/server/whatsapp-rpc/node_modules/commander/typings/esm.d.mts +3 -0
  222. package/server/whatsapp-rpc/node_modules/commander/typings/index.d.ts +884 -0
  223. package/server/whatsapp-rpc/node_modules/cross-spawn/LICENSE +21 -0
  224. package/server/whatsapp-rpc/node_modules/cross-spawn/README.md +89 -0
  225. package/server/whatsapp-rpc/node_modules/cross-spawn/index.js +39 -0
  226. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/enoent.js +59 -0
  227. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/parse.js +91 -0
  228. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/escape.js +47 -0
  229. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/readShebang.js +23 -0
  230. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/resolveCommand.js +52 -0
  231. package/server/whatsapp-rpc/node_modules/cross-spawn/package.json +73 -0
  232. package/server/whatsapp-rpc/node_modules/execa/index.d.ts +955 -0
  233. package/server/whatsapp-rpc/node_modules/execa/index.js +309 -0
  234. package/server/whatsapp-rpc/node_modules/execa/lib/command.js +119 -0
  235. package/server/whatsapp-rpc/node_modules/execa/lib/error.js +87 -0
  236. package/server/whatsapp-rpc/node_modules/execa/lib/kill.js +102 -0
  237. package/server/whatsapp-rpc/node_modules/execa/lib/pipe.js +42 -0
  238. package/server/whatsapp-rpc/node_modules/execa/lib/promise.js +36 -0
  239. package/server/whatsapp-rpc/node_modules/execa/lib/stdio.js +49 -0
  240. package/server/whatsapp-rpc/node_modules/execa/lib/stream.js +133 -0
  241. package/server/whatsapp-rpc/node_modules/execa/lib/verbose.js +19 -0
  242. package/server/whatsapp-rpc/node_modules/execa/license +9 -0
  243. package/server/whatsapp-rpc/node_modules/execa/package.json +90 -0
  244. package/server/whatsapp-rpc/node_modules/execa/readme.md +822 -0
  245. package/server/whatsapp-rpc/node_modules/get-stream/license +9 -0
  246. package/server/whatsapp-rpc/node_modules/get-stream/package.json +53 -0
  247. package/server/whatsapp-rpc/node_modules/get-stream/readme.md +291 -0
  248. package/server/whatsapp-rpc/node_modules/get-stream/source/array-buffer.js +84 -0
  249. package/server/whatsapp-rpc/node_modules/get-stream/source/array.js +32 -0
  250. package/server/whatsapp-rpc/node_modules/get-stream/source/buffer.js +20 -0
  251. package/server/whatsapp-rpc/node_modules/get-stream/source/contents.js +101 -0
  252. package/server/whatsapp-rpc/node_modules/get-stream/source/index.d.ts +119 -0
  253. package/server/whatsapp-rpc/node_modules/get-stream/source/index.js +5 -0
  254. package/server/whatsapp-rpc/node_modules/get-stream/source/string.js +36 -0
  255. package/server/whatsapp-rpc/node_modules/get-stream/source/utils.js +11 -0
  256. package/server/whatsapp-rpc/node_modules/get-them-args/LICENSE +21 -0
  257. package/server/whatsapp-rpc/node_modules/get-them-args/README.md +95 -0
  258. package/server/whatsapp-rpc/node_modules/get-them-args/index.js +97 -0
  259. package/server/whatsapp-rpc/node_modules/get-them-args/package.json +36 -0
  260. package/server/whatsapp-rpc/node_modules/human-signals/LICENSE +201 -0
  261. package/server/whatsapp-rpc/node_modules/human-signals/README.md +168 -0
  262. package/server/whatsapp-rpc/node_modules/human-signals/build/src/core.js +273 -0
  263. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.d.ts +73 -0
  264. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.js +70 -0
  265. package/server/whatsapp-rpc/node_modules/human-signals/build/src/realtime.js +16 -0
  266. package/server/whatsapp-rpc/node_modules/human-signals/build/src/signals.js +34 -0
  267. package/server/whatsapp-rpc/node_modules/human-signals/package.json +61 -0
  268. package/server/whatsapp-rpc/node_modules/is-stream/index.d.ts +81 -0
  269. package/server/whatsapp-rpc/node_modules/is-stream/index.js +29 -0
  270. package/server/whatsapp-rpc/node_modules/is-stream/license +9 -0
  271. package/server/whatsapp-rpc/node_modules/is-stream/package.json +44 -0
  272. package/server/whatsapp-rpc/node_modules/is-stream/readme.md +60 -0
  273. package/server/whatsapp-rpc/node_modules/isexe/LICENSE +15 -0
  274. package/server/whatsapp-rpc/node_modules/isexe/README.md +51 -0
  275. package/server/whatsapp-rpc/node_modules/isexe/index.js +57 -0
  276. package/server/whatsapp-rpc/node_modules/isexe/mode.js +41 -0
  277. package/server/whatsapp-rpc/node_modules/isexe/package.json +31 -0
  278. package/server/whatsapp-rpc/node_modules/isexe/test/basic.js +221 -0
  279. package/server/whatsapp-rpc/node_modules/isexe/windows.js +42 -0
  280. package/server/whatsapp-rpc/node_modules/kill-port/.editorconfig +12 -0
  281. package/server/whatsapp-rpc/node_modules/kill-port/.gitattributes +1 -0
  282. package/server/whatsapp-rpc/node_modules/kill-port/LICENSE +21 -0
  283. package/server/whatsapp-rpc/node_modules/kill-port/README.md +140 -0
  284. package/server/whatsapp-rpc/node_modules/kill-port/cli.js +25 -0
  285. package/server/whatsapp-rpc/node_modules/kill-port/example.js +21 -0
  286. package/server/whatsapp-rpc/node_modules/kill-port/index.js +46 -0
  287. package/server/whatsapp-rpc/node_modules/kill-port/logo.png +0 -0
  288. package/server/whatsapp-rpc/node_modules/kill-port/package.json +41 -0
  289. package/server/whatsapp-rpc/node_modules/kill-port/pnpm-lock.yaml +4606 -0
  290. package/server/whatsapp-rpc/node_modules/kill-port/test.js +16 -0
  291. package/server/whatsapp-rpc/node_modules/merge-stream/LICENSE +21 -0
  292. package/server/whatsapp-rpc/node_modules/merge-stream/README.md +78 -0
  293. package/server/whatsapp-rpc/node_modules/merge-stream/index.js +41 -0
  294. package/server/whatsapp-rpc/node_modules/merge-stream/package.json +19 -0
  295. package/server/whatsapp-rpc/node_modules/mimic-fn/index.d.ts +52 -0
  296. package/server/whatsapp-rpc/node_modules/mimic-fn/index.js +71 -0
  297. package/server/whatsapp-rpc/node_modules/mimic-fn/license +9 -0
  298. package/server/whatsapp-rpc/node_modules/mimic-fn/package.json +45 -0
  299. package/server/whatsapp-rpc/node_modules/mimic-fn/readme.md +90 -0
  300. package/server/whatsapp-rpc/node_modules/npm-run-path/index.d.ts +90 -0
  301. package/server/whatsapp-rpc/node_modules/npm-run-path/index.js +52 -0
  302. package/server/whatsapp-rpc/node_modules/npm-run-path/license +9 -0
  303. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.d.ts +31 -0
  304. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.js +12 -0
  305. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/license +9 -0
  306. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/package.json +41 -0
  307. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/readme.md +57 -0
  308. package/server/whatsapp-rpc/node_modules/npm-run-path/package.json +49 -0
  309. package/server/whatsapp-rpc/node_modules/npm-run-path/readme.md +104 -0
  310. package/server/whatsapp-rpc/node_modules/onetime/index.d.ts +59 -0
  311. package/server/whatsapp-rpc/node_modules/onetime/index.js +41 -0
  312. package/server/whatsapp-rpc/node_modules/onetime/license +9 -0
  313. package/server/whatsapp-rpc/node_modules/onetime/package.json +45 -0
  314. package/server/whatsapp-rpc/node_modules/onetime/readme.md +94 -0
  315. package/server/whatsapp-rpc/node_modules/path-key/index.d.ts +40 -0
  316. package/server/whatsapp-rpc/node_modules/path-key/index.js +16 -0
  317. package/server/whatsapp-rpc/node_modules/path-key/license +9 -0
  318. package/server/whatsapp-rpc/node_modules/path-key/package.json +39 -0
  319. package/server/whatsapp-rpc/node_modules/path-key/readme.md +61 -0
  320. package/server/whatsapp-rpc/node_modules/shebang-command/index.js +19 -0
  321. package/server/whatsapp-rpc/node_modules/shebang-command/license +9 -0
  322. package/server/whatsapp-rpc/node_modules/shebang-command/package.json +34 -0
  323. package/server/whatsapp-rpc/node_modules/shebang-command/readme.md +34 -0
  324. package/server/whatsapp-rpc/node_modules/shebang-regex/index.d.ts +22 -0
  325. package/server/whatsapp-rpc/node_modules/shebang-regex/index.js +2 -0
  326. package/server/whatsapp-rpc/node_modules/shebang-regex/license +9 -0
  327. package/server/whatsapp-rpc/node_modules/shebang-regex/package.json +35 -0
  328. package/server/whatsapp-rpc/node_modules/shebang-regex/readme.md +33 -0
  329. package/server/whatsapp-rpc/node_modules/shell-exec/LICENSE +21 -0
  330. package/server/whatsapp-rpc/node_modules/shell-exec/README.md +60 -0
  331. package/server/whatsapp-rpc/node_modules/shell-exec/index.js +47 -0
  332. package/server/whatsapp-rpc/node_modules/shell-exec/package.json +29 -0
  333. package/server/whatsapp-rpc/node_modules/signal-exit/LICENSE.txt +16 -0
  334. package/server/whatsapp-rpc/node_modules/signal-exit/README.md +74 -0
  335. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts +12 -0
  336. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts.map +1 -0
  337. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js +10 -0
  338. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js.map +1 -0
  339. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts +48 -0
  340. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts.map +1 -0
  341. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js +279 -0
  342. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js.map +1 -0
  343. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/package.json +3 -0
  344. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts +29 -0
  345. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts.map +1 -0
  346. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js +42 -0
  347. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js.map +1 -0
  348. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts +12 -0
  349. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts.map +1 -0
  350. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js +4 -0
  351. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js.map +1 -0
  352. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts +48 -0
  353. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts.map +1 -0
  354. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js +275 -0
  355. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js.map +1 -0
  356. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/package.json +3 -0
  357. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts +29 -0
  358. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts.map +1 -0
  359. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js +39 -0
  360. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js.map +1 -0
  361. package/server/whatsapp-rpc/node_modules/signal-exit/package.json +106 -0
  362. package/server/whatsapp-rpc/node_modules/strip-final-newline/index.js +14 -0
  363. package/server/whatsapp-rpc/node_modules/strip-final-newline/license +9 -0
  364. package/server/whatsapp-rpc/node_modules/strip-final-newline/package.json +43 -0
  365. package/server/whatsapp-rpc/node_modules/strip-final-newline/readme.md +35 -0
  366. package/server/whatsapp-rpc/node_modules/which/CHANGELOG.md +166 -0
  367. package/server/whatsapp-rpc/node_modules/which/LICENSE +15 -0
  368. package/server/whatsapp-rpc/node_modules/which/README.md +54 -0
  369. package/server/whatsapp-rpc/node_modules/which/bin/node-which +52 -0
  370. package/server/whatsapp-rpc/node_modules/which/package.json +43 -0
  371. package/server/whatsapp-rpc/node_modules/which/which.js +125 -0
  372. package/server/whatsapp-rpc/package-lock.json +272 -0
  373. package/server/whatsapp-rpc/package.json +30 -30
  374. package/server/whatsapp-rpc/schema.json +1294 -1294
  375. package/server/whatsapp-rpc/scripts/clean.cjs +66 -66
  376. package/server/whatsapp-rpc/scripts/cli.js +162 -162
  377. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -166
  378. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -15
  379. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -4
  380. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -427
  381. package/server/whatsapp-rpc/web/app.py +609 -609
  382. package/server/whatsapp-rpc/web/requirements.txt +6 -6
  383. package/server/whatsapp-rpc/web/rpc_client.py +427 -427
  384. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -59
  385. package/server/whatsapp-rpc/web/templates/base.html +149 -149
  386. package/server/whatsapp-rpc/web/templates/contacts.html +240 -240
  387. package/server/whatsapp-rpc/web/templates/dashboard.html +319 -319
  388. package/server/whatsapp-rpc/web/templates/groups.html +328 -328
  389. package/server/whatsapp-rpc/web/templates/messages.html +465 -465
  390. package/server/whatsapp-rpc/web/templates/messaging.html +680 -680
  391. package/server/whatsapp-rpc/web/templates/send.html +258 -258
  392. package/server/whatsapp-rpc/web/templates/settings.html +459 -459
  393. package/client/src/components/ui/AndroidSettingsPanel.tsx +0 -401
  394. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +0 -345
  395. package/client/src/nodeDefinitions/androidDeviceNodes.ts +0 -140
  396. package/docker-compose.prod.yml +0 -107
  397. package/docker-compose.yml +0 -104
  398. package/docs-MachinaOs/README.md +0 -85
  399. package/docs-MachinaOs/deployment/docker.mdx +0 -228
  400. package/docs-MachinaOs/deployment/production.mdx +0 -345
  401. package/docs-MachinaOs/docs.json +0 -75
  402. package/docs-MachinaOs/faq.mdx +0 -309
  403. package/docs-MachinaOs/favicon.svg +0 -5
  404. package/docs-MachinaOs/installation.mdx +0 -160
  405. package/docs-MachinaOs/introduction.mdx +0 -114
  406. package/docs-MachinaOs/logo/dark.svg +0 -6
  407. package/docs-MachinaOs/logo/light.svg +0 -6
  408. package/docs-MachinaOs/nodes/ai-agent.mdx +0 -216
  409. package/docs-MachinaOs/nodes/ai-models.mdx +0 -240
  410. package/docs-MachinaOs/nodes/android.mdx +0 -411
  411. package/docs-MachinaOs/nodes/overview.mdx +0 -181
  412. package/docs-MachinaOs/nodes/schedulers.mdx +0 -316
  413. package/docs-MachinaOs/nodes/webhooks.mdx +0 -330
  414. package/docs-MachinaOs/nodes/whatsapp.mdx +0 -305
  415. package/docs-MachinaOs/quickstart.mdx +0 -119
  416. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +0 -177
  417. package/docs-MachinaOs/tutorials/android-automation.mdx +0 -242
  418. package/docs-MachinaOs/tutorials/first-workflow.mdx +0 -134
  419. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +0 -185
  420. package/nul +0 -0
  421. package/scripts/check-ports.ps1 +0 -33
  422. package/scripts/kill-port.ps1 +0 -154
@@ -1,461 +1,461 @@
1
- """Cache service with Redis (production) or SQLite (development) backend.
2
-
3
- Follows n8n pattern where SQLite is sufficient for single-process deployments,
4
- with Redis used only for distributed queue mode or high-performance needs.
5
- """
6
-
7
- import json
8
- import asyncio
9
- from typing import Any, Dict, Optional, List, TYPE_CHECKING
10
- from datetime import timedelta
11
-
12
- try:
13
- import redis.asyncio as redis
14
- REDIS_AVAILABLE = True
15
- except ImportError:
16
- redis = None
17
- REDIS_AVAILABLE = False
18
-
19
- from core.config import Settings
20
- from core.logging import get_logger, log_cache_operation
21
-
22
- if TYPE_CHECKING:
23
- from core.database import Database
24
-
25
- logger = get_logger(__name__)
26
-
27
-
28
- class CacheService:
29
- """Async cache service with Redis or SQLite backend.
30
-
31
- Backend selection:
32
- - Redis: When REDIS_ENABLED=true and Redis is available (production)
33
- - SQLite: When Redis disabled or unavailable (development)
34
- - Memory: Temporary fallback if both fail
35
- """
36
-
37
- def __init__(self, settings: Settings, database: Optional["Database"] = None):
38
- self.settings = settings
39
- self.database = database # SQLite backend
40
- self.redis: Optional[redis.Redis] = None
41
- self.memory_cache: Dict[str, Any] = {} # Emergency fallback only
42
- self.use_redis = settings.redis_enabled and REDIS_AVAILABLE
43
- self.use_sqlite = not self.use_redis and database is not None
44
- self._streams_available = False # Checked during startup
45
-
46
- async def startup(self):
47
- """Initialize cache connection."""
48
- if self.use_redis and self.settings.redis_url:
49
- try:
50
- self.redis = redis.from_url(
51
- self.settings.redis_url,
52
- encoding="utf-8",
53
- decode_responses=True,
54
- socket_timeout=5,
55
- socket_connect_timeout=5,
56
- retry_on_timeout=True
57
- )
58
-
59
- # Test connection
60
- await self.redis.ping()
61
- logger.info("Redis cache initialized", url=self.settings.redis_url)
62
-
63
- # Test Redis Streams availability (required for trigger nodes)
64
- await self._check_streams_support()
65
-
66
- except Exception as e:
67
- logger.warning("Redis connection failed, falling back", error=str(e))
68
- self.use_redis = False
69
- self.redis = None
70
- # Try SQLite fallback
71
- if self.database:
72
- self.use_sqlite = True
73
- logger.info("Using SQLite cache (Redis fallback)")
74
- else:
75
- if self.use_sqlite:
76
- logger.info("Using SQLite cache (n8n pattern - no Redis required for single-process)")
77
- else:
78
- logger.info("Using in-memory cache",
79
- redis_enabled=self.settings.redis_enabled,
80
- redis_available=REDIS_AVAILABLE)
81
-
82
- async def _check_streams_support(self):
83
- """Check if Redis supports Streams (XADD/XREAD commands).
84
-
85
- Some Redis-compatible services (e.g., certain cloud providers) don't support Streams.
86
- We test this at startup to avoid runtime failures in trigger nodes.
87
- """
88
- if not self.redis:
89
- self._streams_available = False
90
- return
91
-
92
- test_stream = "_machina_streams_test"
93
- try:
94
- # Try XADD - this will fail if Streams aren't supported
95
- msg_id = await self.redis.xadd(test_stream, {"test": "1"}, maxlen=1)
96
- if msg_id:
97
- # Clean up test stream
98
- await self.redis.delete(test_stream)
99
- self._streams_available = True
100
- logger.info("Redis Streams available - trigger nodes will use Redis persistence")
101
- else:
102
- self._streams_available = False
103
- logger.warning("Redis Streams test failed - trigger nodes will use memory mode")
104
- except Exception as e:
105
- self._streams_available = False
106
- error_str = str(e).lower()
107
- if "unknown command" in error_str:
108
- logger.warning("Redis Streams not supported by server - trigger nodes will use memory mode")
109
- else:
110
- logger.warning(f"Redis Streams check failed: {e} - trigger nodes will use memory mode")
111
-
112
- async def shutdown(self):
113
- """Close cache connections."""
114
- if self.redis:
115
- await self.redis.close()
116
- logger.info("Redis cache connections closed")
117
-
118
- # Clear memory cache
119
- self.memory_cache.clear()
120
-
121
- async def get(self, key: str) -> Optional[Any]:
122
- """Get value from cache."""
123
- try:
124
- if self.use_redis and self.redis:
125
- value = await self.redis.get(key)
126
- if value:
127
- log_cache_operation(logger, "get", key, hit=True)
128
- return json.loads(value)
129
- else:
130
- log_cache_operation(logger, "get", key, hit=False)
131
- return None
132
- elif self.use_sqlite and self.database:
133
- # SQLite cache
134
- value = await self.database.get_cache_entry(key)
135
- if value:
136
- log_cache_operation(logger, "get", key, hit=True)
137
- return json.loads(value)
138
- else:
139
- log_cache_operation(logger, "get", key, hit=False)
140
- return None
141
- else:
142
- # Memory cache fallback
143
- value = self.memory_cache.get(key)
144
- log_cache_operation(logger, "get", key, hit=value is not None)
145
- return value
146
-
147
- except Exception as e:
148
- logger.error("Cache get failed", key=key, error=str(e))
149
- return None
150
-
151
- async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
152
- """Set value in cache with optional TTL."""
153
- try:
154
- ttl = ttl or self.settings.cache_ttl
155
-
156
- if self.use_redis and self.redis:
157
- serialized = json.dumps(value, default=str)
158
- await self.redis.setex(key, ttl, serialized)
159
- log_cache_operation(logger, "set", key, ttl=ttl)
160
- return True
161
- elif self.use_sqlite and self.database:
162
- # SQLite cache with TTL support
163
- serialized = json.dumps(value, default=str)
164
- await self.database.set_cache_entry(key, serialized, ttl)
165
- log_cache_operation(logger, "set", key, ttl=ttl)
166
- return True
167
- else:
168
- # Memory cache fallback (no TTL)
169
- self.memory_cache[key] = value
170
- log_cache_operation(logger, "set", key, ttl=ttl)
171
- return True
172
-
173
- except Exception as e:
174
- logger.error("Cache set failed", key=key, error=str(e))
175
- return False
176
-
177
- async def delete(self, key: str) -> bool:
178
- """Delete value from cache."""
179
- try:
180
- if self.use_redis and self.redis:
181
- deleted = await self.redis.delete(key)
182
- log_cache_operation(logger, "delete", key, deleted=bool(deleted))
183
- return bool(deleted)
184
- elif self.use_sqlite and self.database:
185
- # SQLite cache
186
- deleted = await self.database.delete_cache_entry(key)
187
- log_cache_operation(logger, "delete", key, deleted=deleted)
188
- return deleted
189
- else:
190
- # Memory cache fallback
191
- deleted = key in self.memory_cache
192
- if deleted:
193
- del self.memory_cache[key]
194
- log_cache_operation(logger, "delete", key, deleted=deleted)
195
- return deleted
196
-
197
- except Exception as e:
198
- logger.error("Cache delete failed", key=key, error=str(e))
199
- return False
200
-
201
- async def exists(self, key: str) -> bool:
202
- """Check if key exists in cache."""
203
- try:
204
- if self.use_redis and self.redis:
205
- exists = await self.redis.exists(key)
206
- return bool(exists)
207
- elif self.use_sqlite and self.database:
208
- return await self.database.cache_exists(key)
209
- else:
210
- return key in self.memory_cache
211
-
212
- except Exception as e:
213
- logger.error("Cache exists check failed", key=key, error=str(e))
214
- return False
215
-
216
- async def expire(self, key: str, ttl: int) -> bool:
217
- """Set TTL for existing key."""
218
- try:
219
- if self.use_redis and self.redis:
220
- return bool(await self.redis.expire(key, ttl))
221
- else:
222
- # Memory cache doesn't support TTL updates
223
- return key in self.memory_cache
224
-
225
- except Exception as e:
226
- logger.error("Cache expire failed", key=key, error=str(e))
227
- return False
228
-
229
- async def clear_pattern(self, pattern: str) -> int:
230
- """Clear keys matching pattern."""
231
- try:
232
- if self.use_redis and self.redis:
233
- keys = await self.redis.keys(pattern)
234
- if keys:
235
- deleted = await self.redis.delete(*keys)
236
- log_cache_operation(logger, "clear_pattern", pattern, deleted=deleted)
237
- return deleted
238
- return 0
239
- elif self.use_sqlite and self.database:
240
- # SQLite cache pattern matching
241
- deleted = await self.database.delete_cache_pattern(pattern)
242
- log_cache_operation(logger, "clear_pattern", pattern, deleted=deleted)
243
- return deleted
244
- else:
245
- # Memory cache pattern matching
246
- keys_to_delete = [k for k in self.memory_cache.keys() if pattern.replace("*", "") in k]
247
- for key in keys_to_delete:
248
- del self.memory_cache[key]
249
- log_cache_operation(logger, "clear_pattern", pattern, deleted=len(keys_to_delete))
250
- return len(keys_to_delete)
251
-
252
- except Exception as e:
253
- logger.error("Cache clear pattern failed", pattern=pattern, error=str(e))
254
- return 0
255
-
256
- # ============================================================================
257
- # API Key Specific Cache Methods
258
- # ============================================================================
259
-
260
- async def cache_api_key(self, provider: str, session_id: str, key_data: Dict[str, Any]) -> bool:
261
- """Cache API key data."""
262
- cache_key = f"api_key:{provider}:{session_id}"
263
- return await self.set(cache_key, key_data, self.settings.api_key_cache_ttl)
264
-
265
- async def get_cached_api_key(self, provider: str, session_id: str) -> Optional[Dict[str, Any]]:
266
- """Get cached API key data."""
267
- cache_key = f"api_key:{provider}:{session_id}"
268
- return await self.get(cache_key)
269
-
270
- async def remove_cached_api_key(self, provider: str, session_id: str) -> bool:
271
- """Remove cached API key."""
272
- cache_key = f"api_key:{provider}:{session_id}"
273
- return await self.delete(cache_key)
274
-
275
- async def cache_models(self, provider: str, models: List[str]) -> bool:
276
- """Cache available models for provider."""
277
- cache_key = f"models:{provider}"
278
- return await self.set(cache_key, {"models": models, "cached_at": "now"},
279
- ttl=3600) # 1 hour
280
-
281
- async def get_cached_models(self, provider: str) -> Optional[List[str]]:
282
- """Get cached models for provider."""
283
- cache_key = f"models:{provider}"
284
- data = await self.get(cache_key)
285
- return data.get("models") if data else None
286
-
287
- # ============================================================================
288
- # Redis Streams Methods for Event Waiting
289
- # ============================================================================
290
-
291
- async def stream_add(self, stream: str, data: Dict[str, Any], maxlen: int = 1000) -> Optional[str]:
292
- """Add message to Redis Stream.
293
-
294
- Args:
295
- stream: Stream name (e.g., 'events:whatsapp_message_received')
296
- data: Event data to store
297
- maxlen: Maximum stream length (approximate, uses ~)
298
-
299
- Returns:
300
- Message ID if successful, None otherwise
301
- """
302
- try:
303
- if self.use_redis and self.redis and self._streams_available:
304
- # Serialize ALL values with json.dumps to preserve types
305
- # This matches the pattern used in set() and ensures proper round-trip:
306
- # - json.dumps(True) → "true" (lowercase, valid JSON)
307
- # - json.loads("true") → True (Python bool)
308
- # Using str() would break: str(True) → "True" → json.loads fails
309
- serialized = {k: json.dumps(v, default=str) for k, v in data.items()}
310
- msg_id = await self.redis.xadd(stream, serialized, maxlen=maxlen, approximate=True)
311
- logger.debug(f"Stream add: {stream} -> {msg_id}")
312
- return msg_id
313
- return None
314
- except Exception as e:
315
- logger.error(f"Stream add failed: {stream}", error=str(e))
316
- return None
317
-
318
- async def stream_read(
319
- self,
320
- streams: Dict[str, str],
321
- count: int = 1,
322
- block: Optional[int] = None
323
- ) -> Optional[List[Any]]:
324
- """Read from Redis Streams.
325
-
326
- Args:
327
- streams: Dict of stream_name -> last_id (use '$' for new messages only, '0' for all)
328
- count: Maximum number of messages to read
329
- block: Milliseconds to block (None = no blocking, 0 = infinite)
330
-
331
- Returns:
332
- List of [stream_name, [(msg_id, data), ...]] or None
333
- """
334
- try:
335
- if self.use_redis and self.redis:
336
- result = await self.redis.xread(streams, count=count, block=block)
337
- return result
338
- return None
339
- except Exception as e:
340
- logger.error(f"Stream read failed: {streams.keys()}", error=str(e))
341
- return None
342
-
343
- async def stream_create_group(
344
- self,
345
- stream: str,
346
- group: str,
347
- start_id: str = '$'
348
- ) -> bool:
349
- """Create consumer group for stream.
350
-
351
- Args:
352
- stream: Stream name
353
- group: Consumer group name
354
- start_id: Start reading from ('$' = new only, '0' = all)
355
-
356
- Returns:
357
- True if created or already exists
358
- """
359
- try:
360
- if self.use_redis and self.redis and self._streams_available:
361
- try:
362
- await self.redis.xgroup_create(stream, group, start_id, mkstream=True)
363
- logger.info(f"Created consumer group: {group} on {stream}")
364
- return True
365
- except Exception as e:
366
- if "BUSYGROUP" in str(e):
367
- # Group already exists - this is fine
368
- return True
369
- raise
370
- return False
371
- except Exception as e:
372
- logger.error(f"Stream create group failed: {stream}/{group}", error=str(e))
373
- return False
374
-
375
- async def stream_read_group(
376
- self,
377
- group: str,
378
- consumer: str,
379
- streams: Dict[str, str],
380
- count: int = 1,
381
- block: Optional[int] = None
382
- ) -> Optional[List[Any]]:
383
- """Read from streams using consumer group.
384
-
385
- Args:
386
- group: Consumer group name
387
- consumer: Consumer name (unique per worker)
388
- streams: Dict of stream_name -> last_id (use '>' for new pending messages)
389
- count: Maximum messages to read
390
- block: Milliseconds to block
391
-
392
- Returns:
393
- List of messages or None
394
- """
395
- try:
396
- if self.use_redis and self.redis and self._streams_available:
397
- result = await self.redis.xreadgroup(
398
- group, consumer, streams,
399
- count=count, block=block
400
- )
401
- return result
402
- return None
403
- except Exception as e:
404
- error_str = str(e).lower()
405
- # Timeout errors are expected during blocking reads - log at debug level
406
- if "timeout" in error_str or "timed out" in error_str:
407
- logger.debug(f"Stream read group timeout: {group}/{consumer}", error=str(e))
408
- else:
409
- logger.error(f"Stream read group failed: {group}/{consumer}", error=str(e))
410
- return None
411
-
412
- async def stream_ack(self, stream: str, group: str, *msg_ids: str) -> int:
413
- """Acknowledge messages in consumer group.
414
-
415
- Args:
416
- stream: Stream name
417
- group: Consumer group name
418
- msg_ids: Message IDs to acknowledge
419
-
420
- Returns:
421
- Number of messages acknowledged
422
- """
423
- try:
424
- if self.use_redis and self.redis:
425
- count = await self.redis.xack(stream, group, *msg_ids)
426
- return count
427
- return 0
428
- except Exception as e:
429
- logger.error(f"Stream ack failed: {stream}/{group}", error=str(e))
430
- return 0
431
-
432
- async def stream_delete(self, stream: str, *msg_ids: str) -> int:
433
- """Delete messages from stream.
434
-
435
- Args:
436
- stream: Stream name
437
- msg_ids: Message IDs to delete
438
-
439
- Returns:
440
- Number of messages deleted
441
- """
442
- try:
443
- if self.use_redis and self.redis:
444
- count = await self.redis.xdel(stream, *msg_ids)
445
- return count
446
- return 0
447
- except Exception as e:
448
- logger.error(f"Stream delete failed: {stream}", error=str(e))
449
- return 0
450
-
451
- def is_redis_available(self) -> bool:
452
- """Check if Redis is available and connected."""
453
- return self.use_redis and self.redis is not None
454
-
455
- def is_streams_available(self) -> bool:
456
- """Check if Redis Streams are available (for trigger nodes).
457
-
458
- Returns True only if Redis is connected AND supports Streams commands.
459
- This is checked once during startup to avoid runtime failures.
460
- """
1
+ """Cache service with Redis (production) or SQLite (development) backend.
2
+
3
+ Follows n8n pattern where SQLite is sufficient for single-process deployments,
4
+ with Redis used only for distributed queue mode or high-performance needs.
5
+ """
6
+
7
+ import json
8
+ import asyncio
9
+ from typing import Any, Dict, Optional, List, TYPE_CHECKING
10
+ from datetime import timedelta
11
+
12
+ try:
13
+ import redis.asyncio as redis
14
+ REDIS_AVAILABLE = True
15
+ except ImportError:
16
+ redis = None
17
+ REDIS_AVAILABLE = False
18
+
19
+ from core.config import Settings
20
+ from core.logging import get_logger, log_cache_operation
21
+
22
+ if TYPE_CHECKING:
23
+ from core.database import Database
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class CacheService:
29
+ """Async cache service with Redis or SQLite backend.
30
+
31
+ Backend selection:
32
+ - Redis: When REDIS_ENABLED=true and Redis is available (production)
33
+ - SQLite: When Redis disabled or unavailable (development)
34
+ - Memory: Temporary fallback if both fail
35
+ """
36
+
37
+ def __init__(self, settings: Settings, database: Optional["Database"] = None):
38
+ self.settings = settings
39
+ self.database = database # SQLite backend
40
+ self.redis: Optional[redis.Redis] = None
41
+ self.memory_cache: Dict[str, Any] = {} # Emergency fallback only
42
+ self.use_redis = settings.redis_enabled and REDIS_AVAILABLE
43
+ self.use_sqlite = not self.use_redis and database is not None
44
+ self._streams_available = False # Checked during startup
45
+
46
+ async def startup(self):
47
+ """Initialize cache connection."""
48
+ if self.use_redis and self.settings.redis_url:
49
+ try:
50
+ self.redis = redis.from_url(
51
+ self.settings.redis_url,
52
+ encoding="utf-8",
53
+ decode_responses=True,
54
+ socket_timeout=5,
55
+ socket_connect_timeout=5,
56
+ retry_on_timeout=True
57
+ )
58
+
59
+ # Test connection
60
+ await self.redis.ping()
61
+ logger.info("Redis cache initialized", url=self.settings.redis_url)
62
+
63
+ # Test Redis Streams availability (required for trigger nodes)
64
+ await self._check_streams_support()
65
+
66
+ except Exception as e:
67
+ logger.warning("Redis connection failed, falling back", error=str(e))
68
+ self.use_redis = False
69
+ self.redis = None
70
+ # Try SQLite fallback
71
+ if self.database:
72
+ self.use_sqlite = True
73
+ logger.info("Using SQLite cache (Redis fallback)")
74
+ else:
75
+ if self.use_sqlite:
76
+ logger.info("Using SQLite cache (n8n pattern - no Redis required for single-process)")
77
+ else:
78
+ logger.info("Using in-memory cache",
79
+ redis_enabled=self.settings.redis_enabled,
80
+ redis_available=REDIS_AVAILABLE)
81
+
82
+ async def _check_streams_support(self):
83
+ """Check if Redis supports Streams (XADD/XREAD commands).
84
+
85
+ Some Redis-compatible services (e.g., certain cloud providers) don't support Streams.
86
+ We test this at startup to avoid runtime failures in trigger nodes.
87
+ """
88
+ if not self.redis:
89
+ self._streams_available = False
90
+ return
91
+
92
+ test_stream = "_machina_streams_test"
93
+ try:
94
+ # Try XADD - this will fail if Streams aren't supported
95
+ msg_id = await self.redis.xadd(test_stream, {"test": "1"}, maxlen=1)
96
+ if msg_id:
97
+ # Clean up test stream
98
+ await self.redis.delete(test_stream)
99
+ self._streams_available = True
100
+ logger.info("Redis Streams available - trigger nodes will use Redis persistence")
101
+ else:
102
+ self._streams_available = False
103
+ logger.warning("Redis Streams test failed - trigger nodes will use memory mode")
104
+ except Exception as e:
105
+ self._streams_available = False
106
+ error_str = str(e).lower()
107
+ if "unknown command" in error_str:
108
+ logger.warning("Redis Streams not supported by server - trigger nodes will use memory mode")
109
+ else:
110
+ logger.warning(f"Redis Streams check failed: {e} - trigger nodes will use memory mode")
111
+
112
+ async def shutdown(self):
113
+ """Close cache connections."""
114
+ if self.redis:
115
+ await self.redis.close()
116
+ logger.info("Redis cache connections closed")
117
+
118
+ # Clear memory cache
119
+ self.memory_cache.clear()
120
+
121
+ async def get(self, key: str) -> Optional[Any]:
122
+ """Get value from cache."""
123
+ try:
124
+ if self.use_redis and self.redis:
125
+ value = await self.redis.get(key)
126
+ if value:
127
+ log_cache_operation(logger, "get", key, hit=True)
128
+ return json.loads(value)
129
+ else:
130
+ log_cache_operation(logger, "get", key, hit=False)
131
+ return None
132
+ elif self.use_sqlite and self.database:
133
+ # SQLite cache
134
+ value = await self.database.get_cache_entry(key)
135
+ if value:
136
+ log_cache_operation(logger, "get", key, hit=True)
137
+ return json.loads(value)
138
+ else:
139
+ log_cache_operation(logger, "get", key, hit=False)
140
+ return None
141
+ else:
142
+ # Memory cache fallback
143
+ value = self.memory_cache.get(key)
144
+ log_cache_operation(logger, "get", key, hit=value is not None)
145
+ return value
146
+
147
+ except Exception as e:
148
+ logger.error("Cache get failed", key=key, error=str(e))
149
+ return None
150
+
151
+ async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
152
+ """Set value in cache with optional TTL."""
153
+ try:
154
+ ttl = ttl or self.settings.cache_ttl
155
+
156
+ if self.use_redis and self.redis:
157
+ serialized = json.dumps(value, default=str)
158
+ await self.redis.setex(key, ttl, serialized)
159
+ log_cache_operation(logger, "set", key, ttl=ttl)
160
+ return True
161
+ elif self.use_sqlite and self.database:
162
+ # SQLite cache with TTL support
163
+ serialized = json.dumps(value, default=str)
164
+ await self.database.set_cache_entry(key, serialized, ttl)
165
+ log_cache_operation(logger, "set", key, ttl=ttl)
166
+ return True
167
+ else:
168
+ # Memory cache fallback (no TTL)
169
+ self.memory_cache[key] = value
170
+ log_cache_operation(logger, "set", key, ttl=ttl)
171
+ return True
172
+
173
+ except Exception as e:
174
+ logger.error("Cache set failed", key=key, error=str(e))
175
+ return False
176
+
177
+ async def delete(self, key: str) -> bool:
178
+ """Delete value from cache."""
179
+ try:
180
+ if self.use_redis and self.redis:
181
+ deleted = await self.redis.delete(key)
182
+ log_cache_operation(logger, "delete", key, deleted=bool(deleted))
183
+ return bool(deleted)
184
+ elif self.use_sqlite and self.database:
185
+ # SQLite cache
186
+ deleted = await self.database.delete_cache_entry(key)
187
+ log_cache_operation(logger, "delete", key, deleted=deleted)
188
+ return deleted
189
+ else:
190
+ # Memory cache fallback
191
+ deleted = key in self.memory_cache
192
+ if deleted:
193
+ del self.memory_cache[key]
194
+ log_cache_operation(logger, "delete", key, deleted=deleted)
195
+ return deleted
196
+
197
+ except Exception as e:
198
+ logger.error("Cache delete failed", key=key, error=str(e))
199
+ return False
200
+
201
+ async def exists(self, key: str) -> bool:
202
+ """Check if key exists in cache."""
203
+ try:
204
+ if self.use_redis and self.redis:
205
+ exists = await self.redis.exists(key)
206
+ return bool(exists)
207
+ elif self.use_sqlite and self.database:
208
+ return await self.database.cache_exists(key)
209
+ else:
210
+ return key in self.memory_cache
211
+
212
+ except Exception as e:
213
+ logger.error("Cache exists check failed", key=key, error=str(e))
214
+ return False
215
+
216
+ async def expire(self, key: str, ttl: int) -> bool:
217
+ """Set TTL for existing key."""
218
+ try:
219
+ if self.use_redis and self.redis:
220
+ return bool(await self.redis.expire(key, ttl))
221
+ else:
222
+ # Memory cache doesn't support TTL updates
223
+ return key in self.memory_cache
224
+
225
+ except Exception as e:
226
+ logger.error("Cache expire failed", key=key, error=str(e))
227
+ return False
228
+
229
+ async def clear_pattern(self, pattern: str) -> int:
230
+ """Clear keys matching pattern."""
231
+ try:
232
+ if self.use_redis and self.redis:
233
+ keys = await self.redis.keys(pattern)
234
+ if keys:
235
+ deleted = await self.redis.delete(*keys)
236
+ log_cache_operation(logger, "clear_pattern", pattern, deleted=deleted)
237
+ return deleted
238
+ return 0
239
+ elif self.use_sqlite and self.database:
240
+ # SQLite cache pattern matching
241
+ deleted = await self.database.delete_cache_pattern(pattern)
242
+ log_cache_operation(logger, "clear_pattern", pattern, deleted=deleted)
243
+ return deleted
244
+ else:
245
+ # Memory cache pattern matching
246
+ keys_to_delete = [k for k in self.memory_cache.keys() if pattern.replace("*", "") in k]
247
+ for key in keys_to_delete:
248
+ del self.memory_cache[key]
249
+ log_cache_operation(logger, "clear_pattern", pattern, deleted=len(keys_to_delete))
250
+ return len(keys_to_delete)
251
+
252
+ except Exception as e:
253
+ logger.error("Cache clear pattern failed", pattern=pattern, error=str(e))
254
+ return 0
255
+
256
+ # ============================================================================
257
+ # API Key Specific Cache Methods
258
+ # ============================================================================
259
+
260
+ async def cache_api_key(self, provider: str, session_id: str, key_data: Dict[str, Any]) -> bool:
261
+ """Cache API key data."""
262
+ cache_key = f"api_key:{provider}:{session_id}"
263
+ return await self.set(cache_key, key_data, self.settings.api_key_cache_ttl)
264
+
265
+ async def get_cached_api_key(self, provider: str, session_id: str) -> Optional[Dict[str, Any]]:
266
+ """Get cached API key data."""
267
+ cache_key = f"api_key:{provider}:{session_id}"
268
+ return await self.get(cache_key)
269
+
270
+ async def remove_cached_api_key(self, provider: str, session_id: str) -> bool:
271
+ """Remove cached API key."""
272
+ cache_key = f"api_key:{provider}:{session_id}"
273
+ return await self.delete(cache_key)
274
+
275
+ async def cache_models(self, provider: str, models: List[str]) -> bool:
276
+ """Cache available models for provider."""
277
+ cache_key = f"models:{provider}"
278
+ return await self.set(cache_key, {"models": models, "cached_at": "now"},
279
+ ttl=3600) # 1 hour
280
+
281
+ async def get_cached_models(self, provider: str) -> Optional[List[str]]:
282
+ """Get cached models for provider."""
283
+ cache_key = f"models:{provider}"
284
+ data = await self.get(cache_key)
285
+ return data.get("models") if data else None
286
+
287
+ # ============================================================================
288
+ # Redis Streams Methods for Event Waiting
289
+ # ============================================================================
290
+
291
+ async def stream_add(self, stream: str, data: Dict[str, Any], maxlen: int = 1000) -> Optional[str]:
292
+ """Add message to Redis Stream.
293
+
294
+ Args:
295
+ stream: Stream name (e.g., 'events:whatsapp_message_received')
296
+ data: Event data to store
297
+ maxlen: Maximum stream length (approximate, uses ~)
298
+
299
+ Returns:
300
+ Message ID if successful, None otherwise
301
+ """
302
+ try:
303
+ if self.use_redis and self.redis and self._streams_available:
304
+ # Serialize ALL values with json.dumps to preserve types
305
+ # This matches the pattern used in set() and ensures proper round-trip:
306
+ # - json.dumps(True) → "true" (lowercase, valid JSON)
307
+ # - json.loads("true") → True (Python bool)
308
+ # Using str() would break: str(True) → "True" → json.loads fails
309
+ serialized = {k: json.dumps(v, default=str) for k, v in data.items()}
310
+ msg_id = await self.redis.xadd(stream, serialized, maxlen=maxlen, approximate=True)
311
+ logger.debug(f"Stream add: {stream} -> {msg_id}")
312
+ return msg_id
313
+ return None
314
+ except Exception as e:
315
+ logger.error(f"Stream add failed: {stream}", error=str(e))
316
+ return None
317
+
318
+ async def stream_read(
319
+ self,
320
+ streams: Dict[str, str],
321
+ count: int = 1,
322
+ block: Optional[int] = None
323
+ ) -> Optional[List[Any]]:
324
+ """Read from Redis Streams.
325
+
326
+ Args:
327
+ streams: Dict of stream_name -> last_id (use '$' for new messages only, '0' for all)
328
+ count: Maximum number of messages to read
329
+ block: Milliseconds to block (None = no blocking, 0 = infinite)
330
+
331
+ Returns:
332
+ List of [stream_name, [(msg_id, data), ...]] or None
333
+ """
334
+ try:
335
+ if self.use_redis and self.redis:
336
+ result = await self.redis.xread(streams, count=count, block=block)
337
+ return result
338
+ return None
339
+ except Exception as e:
340
+ logger.error(f"Stream read failed: {streams.keys()}", error=str(e))
341
+ return None
342
+
343
+ async def stream_create_group(
344
+ self,
345
+ stream: str,
346
+ group: str,
347
+ start_id: str = '$'
348
+ ) -> bool:
349
+ """Create consumer group for stream.
350
+
351
+ Args:
352
+ stream: Stream name
353
+ group: Consumer group name
354
+ start_id: Start reading from ('$' = new only, '0' = all)
355
+
356
+ Returns:
357
+ True if created or already exists
358
+ """
359
+ try:
360
+ if self.use_redis and self.redis and self._streams_available:
361
+ try:
362
+ await self.redis.xgroup_create(stream, group, start_id, mkstream=True)
363
+ logger.info(f"Created consumer group: {group} on {stream}")
364
+ return True
365
+ except Exception as e:
366
+ if "BUSYGROUP" in str(e):
367
+ # Group already exists - this is fine
368
+ return True
369
+ raise
370
+ return False
371
+ except Exception as e:
372
+ logger.error(f"Stream create group failed: {stream}/{group}", error=str(e))
373
+ return False
374
+
375
+ async def stream_read_group(
376
+ self,
377
+ group: str,
378
+ consumer: str,
379
+ streams: Dict[str, str],
380
+ count: int = 1,
381
+ block: Optional[int] = None
382
+ ) -> Optional[List[Any]]:
383
+ """Read from streams using consumer group.
384
+
385
+ Args:
386
+ group: Consumer group name
387
+ consumer: Consumer name (unique per worker)
388
+ streams: Dict of stream_name -> last_id (use '>' for new pending messages)
389
+ count: Maximum messages to read
390
+ block: Milliseconds to block
391
+
392
+ Returns:
393
+ List of messages or None
394
+ """
395
+ try:
396
+ if self.use_redis and self.redis and self._streams_available:
397
+ result = await self.redis.xreadgroup(
398
+ group, consumer, streams,
399
+ count=count, block=block
400
+ )
401
+ return result
402
+ return None
403
+ except Exception as e:
404
+ error_str = str(e).lower()
405
+ # Timeout errors are expected during blocking reads - log at debug level
406
+ if "timeout" in error_str or "timed out" in error_str:
407
+ logger.debug(f"Stream read group timeout: {group}/{consumer}", error=str(e))
408
+ else:
409
+ logger.error(f"Stream read group failed: {group}/{consumer}", error=str(e))
410
+ return None
411
+
412
+ async def stream_ack(self, stream: str, group: str, *msg_ids: str) -> int:
413
+ """Acknowledge messages in consumer group.
414
+
415
+ Args:
416
+ stream: Stream name
417
+ group: Consumer group name
418
+ msg_ids: Message IDs to acknowledge
419
+
420
+ Returns:
421
+ Number of messages acknowledged
422
+ """
423
+ try:
424
+ if self.use_redis and self.redis:
425
+ count = await self.redis.xack(stream, group, *msg_ids)
426
+ return count
427
+ return 0
428
+ except Exception as e:
429
+ logger.error(f"Stream ack failed: {stream}/{group}", error=str(e))
430
+ return 0
431
+
432
+ async def stream_delete(self, stream: str, *msg_ids: str) -> int:
433
+ """Delete messages from stream.
434
+
435
+ Args:
436
+ stream: Stream name
437
+ msg_ids: Message IDs to delete
438
+
439
+ Returns:
440
+ Number of messages deleted
441
+ """
442
+ try:
443
+ if self.use_redis and self.redis:
444
+ count = await self.redis.xdel(stream, *msg_ids)
445
+ return count
446
+ return 0
447
+ except Exception as e:
448
+ logger.error(f"Stream delete failed: {stream}", error=str(e))
449
+ return 0
450
+
451
+ def is_redis_available(self) -> bool:
452
+ """Check if Redis is available and connected."""
453
+ return self.use_redis and self.redis is not None
454
+
455
+ def is_streams_available(self) -> bool:
456
+ """Check if Redis Streams are available (for trigger nodes).
457
+
458
+ Returns True only if Redis is connected AND supports Streams commands.
459
+ This is checked once during startup to avoid runtime failures.
460
+ """
461
461
  return self.use_redis and self.redis is not None and self._streams_available