machinaos 0.0.1 → 0.0.6

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 (420) hide show
  1. package/.env.template +71 -71
  2. package/LICENSE +21 -21
  3. package/README.md +145 -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/package.json +78 -70
  114. package/scripts/build.js +153 -45
  115. package/scripts/clean.js +40 -40
  116. package/scripts/start.js +234 -210
  117. package/scripts/stop.js +301 -325
  118. package/server/.dockerignore +44 -44
  119. package/server/Dockerfile +45 -45
  120. package/server/constants.py +244 -249
  121. package/server/core/cache.py +460 -460
  122. package/server/core/config.py +127 -127
  123. package/server/core/container.py +98 -98
  124. package/server/core/database.py +1296 -1210
  125. package/server/core/logging.py +313 -313
  126. package/server/main.py +288 -288
  127. package/server/middleware/__init__.py +5 -5
  128. package/server/middleware/auth.py +89 -89
  129. package/server/models/auth.py +52 -52
  130. package/server/models/cache.py +24 -24
  131. package/server/models/database.py +235 -210
  132. package/server/models/nodes.py +435 -455
  133. package/server/pyproject.toml +75 -72
  134. package/server/requirements.txt +83 -83
  135. package/server/routers/android.py +294 -294
  136. package/server/routers/auth.py +203 -203
  137. package/server/routers/database.py +150 -150
  138. package/server/routers/maps.py +141 -141
  139. package/server/routers/nodejs_compat.py +288 -288
  140. package/server/routers/webhook.py +90 -90
  141. package/server/routers/websocket.py +2239 -2127
  142. package/server/routers/whatsapp.py +761 -761
  143. package/server/routers/workflow.py +199 -199
  144. package/server/services/ai.py +2444 -2414
  145. package/server/services/android_service.py +588 -588
  146. package/server/services/auth.py +130 -130
  147. package/server/services/chat_client.py +160 -160
  148. package/server/services/deployment/manager.py +706 -706
  149. package/server/services/event_waiter.py +675 -785
  150. package/server/services/execution/executor.py +1351 -1351
  151. package/server/services/execution/models.py +1 -1
  152. package/server/services/handlers/__init__.py +122 -126
  153. package/server/services/handlers/ai.py +390 -355
  154. package/server/services/handlers/android.py +69 -260
  155. package/server/services/handlers/code.py +278 -278
  156. package/server/services/handlers/http.py +193 -193
  157. package/server/services/handlers/tools.py +146 -32
  158. package/server/services/handlers/triggers.py +107 -107
  159. package/server/services/handlers/utility.py +822 -822
  160. package/server/services/handlers/whatsapp.py +423 -476
  161. package/server/services/maps.py +288 -288
  162. package/server/services/memory_store.py +103 -103
  163. package/server/services/node_executor.py +372 -375
  164. package/server/services/scheduler.py +155 -155
  165. package/server/services/skill_loader.py +1 -1
  166. package/server/services/status_broadcaster.py +834 -826
  167. package/server/services/temporal/__init__.py +23 -23
  168. package/server/services/temporal/activities.py +344 -344
  169. package/server/services/temporal/client.py +76 -76
  170. package/server/services/temporal/executor.py +147 -147
  171. package/server/services/temporal/worker.py +251 -251
  172. package/server/services/temporal/workflow.py +355 -355
  173. package/server/services/temporal/ws_client.py +236 -236
  174. package/server/services/text.py +110 -110
  175. package/server/services/user_auth.py +172 -172
  176. package/server/services/websocket_client.py +29 -29
  177. package/server/services/workflow.py +597 -597
  178. package/server/skills/android-skill/SKILL.md +4 -4
  179. package/server/skills/code-skill/SKILL.md +123 -89
  180. package/server/skills/maps-skill/SKILL.md +3 -3
  181. package/server/skills/memory-skill/SKILL.md +1 -1
  182. package/server/skills/web-search-skill/SKILL.md +154 -0
  183. package/server/skills/whatsapp-skill/SKILL.md +3 -3
  184. package/server/uv.lock +461 -100
  185. package/server/whatsapp-rpc/.dockerignore +30 -30
  186. package/server/whatsapp-rpc/Dockerfile +44 -44
  187. package/server/whatsapp-rpc/Dockerfile.web +17 -17
  188. package/server/whatsapp-rpc/README.md +139 -139
  189. package/server/whatsapp-rpc/bin/whatsapp-rpc-server +0 -0
  190. package/server/whatsapp-rpc/cli.js +95 -95
  191. package/server/whatsapp-rpc/configs/config.yaml +6 -6
  192. package/server/whatsapp-rpc/docker-compose.yml +35 -35
  193. package/server/whatsapp-rpc/docs/API.md +410 -410
  194. package/server/whatsapp-rpc/node_modules/.package-lock.json +259 -0
  195. package/server/whatsapp-rpc/node_modules/chalk/license +9 -0
  196. package/server/whatsapp-rpc/node_modules/chalk/package.json +83 -0
  197. package/server/whatsapp-rpc/node_modules/chalk/readme.md +297 -0
  198. package/server/whatsapp-rpc/node_modules/chalk/source/index.d.ts +325 -0
  199. package/server/whatsapp-rpc/node_modules/chalk/source/index.js +225 -0
  200. package/server/whatsapp-rpc/node_modules/chalk/source/utilities.js +33 -0
  201. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  202. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  203. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  204. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  205. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  206. package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  207. package/server/whatsapp-rpc/node_modules/commander/LICENSE +22 -0
  208. package/server/whatsapp-rpc/node_modules/commander/Readme.md +1148 -0
  209. package/server/whatsapp-rpc/node_modules/commander/esm.mjs +16 -0
  210. package/server/whatsapp-rpc/node_modules/commander/index.js +26 -0
  211. package/server/whatsapp-rpc/node_modules/commander/lib/argument.js +145 -0
  212. package/server/whatsapp-rpc/node_modules/commander/lib/command.js +2179 -0
  213. package/server/whatsapp-rpc/node_modules/commander/lib/error.js +43 -0
  214. package/server/whatsapp-rpc/node_modules/commander/lib/help.js +462 -0
  215. package/server/whatsapp-rpc/node_modules/commander/lib/option.js +329 -0
  216. package/server/whatsapp-rpc/node_modules/commander/lib/suggestSimilar.js +100 -0
  217. package/server/whatsapp-rpc/node_modules/commander/package-support.json +16 -0
  218. package/server/whatsapp-rpc/node_modules/commander/package.json +80 -0
  219. package/server/whatsapp-rpc/node_modules/commander/typings/esm.d.mts +3 -0
  220. package/server/whatsapp-rpc/node_modules/commander/typings/index.d.ts +884 -0
  221. package/server/whatsapp-rpc/node_modules/cross-spawn/LICENSE +21 -0
  222. package/server/whatsapp-rpc/node_modules/cross-spawn/README.md +89 -0
  223. package/server/whatsapp-rpc/node_modules/cross-spawn/index.js +39 -0
  224. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/enoent.js +59 -0
  225. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/parse.js +91 -0
  226. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/escape.js +47 -0
  227. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/readShebang.js +23 -0
  228. package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/resolveCommand.js +52 -0
  229. package/server/whatsapp-rpc/node_modules/cross-spawn/package.json +73 -0
  230. package/server/whatsapp-rpc/node_modules/execa/index.d.ts +955 -0
  231. package/server/whatsapp-rpc/node_modules/execa/index.js +309 -0
  232. package/server/whatsapp-rpc/node_modules/execa/lib/command.js +119 -0
  233. package/server/whatsapp-rpc/node_modules/execa/lib/error.js +87 -0
  234. package/server/whatsapp-rpc/node_modules/execa/lib/kill.js +102 -0
  235. package/server/whatsapp-rpc/node_modules/execa/lib/pipe.js +42 -0
  236. package/server/whatsapp-rpc/node_modules/execa/lib/promise.js +36 -0
  237. package/server/whatsapp-rpc/node_modules/execa/lib/stdio.js +49 -0
  238. package/server/whatsapp-rpc/node_modules/execa/lib/stream.js +133 -0
  239. package/server/whatsapp-rpc/node_modules/execa/lib/verbose.js +19 -0
  240. package/server/whatsapp-rpc/node_modules/execa/license +9 -0
  241. package/server/whatsapp-rpc/node_modules/execa/package.json +90 -0
  242. package/server/whatsapp-rpc/node_modules/execa/readme.md +822 -0
  243. package/server/whatsapp-rpc/node_modules/get-stream/license +9 -0
  244. package/server/whatsapp-rpc/node_modules/get-stream/package.json +53 -0
  245. package/server/whatsapp-rpc/node_modules/get-stream/readme.md +291 -0
  246. package/server/whatsapp-rpc/node_modules/get-stream/source/array-buffer.js +84 -0
  247. package/server/whatsapp-rpc/node_modules/get-stream/source/array.js +32 -0
  248. package/server/whatsapp-rpc/node_modules/get-stream/source/buffer.js +20 -0
  249. package/server/whatsapp-rpc/node_modules/get-stream/source/contents.js +101 -0
  250. package/server/whatsapp-rpc/node_modules/get-stream/source/index.d.ts +119 -0
  251. package/server/whatsapp-rpc/node_modules/get-stream/source/index.js +5 -0
  252. package/server/whatsapp-rpc/node_modules/get-stream/source/string.js +36 -0
  253. package/server/whatsapp-rpc/node_modules/get-stream/source/utils.js +11 -0
  254. package/server/whatsapp-rpc/node_modules/get-them-args/LICENSE +21 -0
  255. package/server/whatsapp-rpc/node_modules/get-them-args/README.md +95 -0
  256. package/server/whatsapp-rpc/node_modules/get-them-args/index.js +97 -0
  257. package/server/whatsapp-rpc/node_modules/get-them-args/package.json +36 -0
  258. package/server/whatsapp-rpc/node_modules/human-signals/LICENSE +201 -0
  259. package/server/whatsapp-rpc/node_modules/human-signals/README.md +168 -0
  260. package/server/whatsapp-rpc/node_modules/human-signals/build/src/core.js +273 -0
  261. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.d.ts +73 -0
  262. package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.js +70 -0
  263. package/server/whatsapp-rpc/node_modules/human-signals/build/src/realtime.js +16 -0
  264. package/server/whatsapp-rpc/node_modules/human-signals/build/src/signals.js +34 -0
  265. package/server/whatsapp-rpc/node_modules/human-signals/package.json +61 -0
  266. package/server/whatsapp-rpc/node_modules/is-stream/index.d.ts +81 -0
  267. package/server/whatsapp-rpc/node_modules/is-stream/index.js +29 -0
  268. package/server/whatsapp-rpc/node_modules/is-stream/license +9 -0
  269. package/server/whatsapp-rpc/node_modules/is-stream/package.json +44 -0
  270. package/server/whatsapp-rpc/node_modules/is-stream/readme.md +60 -0
  271. package/server/whatsapp-rpc/node_modules/isexe/LICENSE +15 -0
  272. package/server/whatsapp-rpc/node_modules/isexe/README.md +51 -0
  273. package/server/whatsapp-rpc/node_modules/isexe/index.js +57 -0
  274. package/server/whatsapp-rpc/node_modules/isexe/mode.js +41 -0
  275. package/server/whatsapp-rpc/node_modules/isexe/package.json +31 -0
  276. package/server/whatsapp-rpc/node_modules/isexe/test/basic.js +221 -0
  277. package/server/whatsapp-rpc/node_modules/isexe/windows.js +42 -0
  278. package/server/whatsapp-rpc/node_modules/kill-port/.editorconfig +12 -0
  279. package/server/whatsapp-rpc/node_modules/kill-port/.gitattributes +1 -0
  280. package/server/whatsapp-rpc/node_modules/kill-port/LICENSE +21 -0
  281. package/server/whatsapp-rpc/node_modules/kill-port/README.md +140 -0
  282. package/server/whatsapp-rpc/node_modules/kill-port/cli.js +25 -0
  283. package/server/whatsapp-rpc/node_modules/kill-port/example.js +21 -0
  284. package/server/whatsapp-rpc/node_modules/kill-port/index.js +46 -0
  285. package/server/whatsapp-rpc/node_modules/kill-port/logo.png +0 -0
  286. package/server/whatsapp-rpc/node_modules/kill-port/package.json +41 -0
  287. package/server/whatsapp-rpc/node_modules/kill-port/pnpm-lock.yaml +4606 -0
  288. package/server/whatsapp-rpc/node_modules/kill-port/test.js +16 -0
  289. package/server/whatsapp-rpc/node_modules/merge-stream/LICENSE +21 -0
  290. package/server/whatsapp-rpc/node_modules/merge-stream/README.md +78 -0
  291. package/server/whatsapp-rpc/node_modules/merge-stream/index.js +41 -0
  292. package/server/whatsapp-rpc/node_modules/merge-stream/package.json +19 -0
  293. package/server/whatsapp-rpc/node_modules/mimic-fn/index.d.ts +52 -0
  294. package/server/whatsapp-rpc/node_modules/mimic-fn/index.js +71 -0
  295. package/server/whatsapp-rpc/node_modules/mimic-fn/license +9 -0
  296. package/server/whatsapp-rpc/node_modules/mimic-fn/package.json +45 -0
  297. package/server/whatsapp-rpc/node_modules/mimic-fn/readme.md +90 -0
  298. package/server/whatsapp-rpc/node_modules/npm-run-path/index.d.ts +90 -0
  299. package/server/whatsapp-rpc/node_modules/npm-run-path/index.js +52 -0
  300. package/server/whatsapp-rpc/node_modules/npm-run-path/license +9 -0
  301. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.d.ts +31 -0
  302. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.js +12 -0
  303. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/license +9 -0
  304. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/package.json +41 -0
  305. package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/readme.md +57 -0
  306. package/server/whatsapp-rpc/node_modules/npm-run-path/package.json +49 -0
  307. package/server/whatsapp-rpc/node_modules/npm-run-path/readme.md +104 -0
  308. package/server/whatsapp-rpc/node_modules/onetime/index.d.ts +59 -0
  309. package/server/whatsapp-rpc/node_modules/onetime/index.js +41 -0
  310. package/server/whatsapp-rpc/node_modules/onetime/license +9 -0
  311. package/server/whatsapp-rpc/node_modules/onetime/package.json +45 -0
  312. package/server/whatsapp-rpc/node_modules/onetime/readme.md +94 -0
  313. package/server/whatsapp-rpc/node_modules/path-key/index.d.ts +40 -0
  314. package/server/whatsapp-rpc/node_modules/path-key/index.js +16 -0
  315. package/server/whatsapp-rpc/node_modules/path-key/license +9 -0
  316. package/server/whatsapp-rpc/node_modules/path-key/package.json +39 -0
  317. package/server/whatsapp-rpc/node_modules/path-key/readme.md +61 -0
  318. package/server/whatsapp-rpc/node_modules/shebang-command/index.js +19 -0
  319. package/server/whatsapp-rpc/node_modules/shebang-command/license +9 -0
  320. package/server/whatsapp-rpc/node_modules/shebang-command/package.json +34 -0
  321. package/server/whatsapp-rpc/node_modules/shebang-command/readme.md +34 -0
  322. package/server/whatsapp-rpc/node_modules/shebang-regex/index.d.ts +22 -0
  323. package/server/whatsapp-rpc/node_modules/shebang-regex/index.js +2 -0
  324. package/server/whatsapp-rpc/node_modules/shebang-regex/license +9 -0
  325. package/server/whatsapp-rpc/node_modules/shebang-regex/package.json +35 -0
  326. package/server/whatsapp-rpc/node_modules/shebang-regex/readme.md +33 -0
  327. package/server/whatsapp-rpc/node_modules/shell-exec/LICENSE +21 -0
  328. package/server/whatsapp-rpc/node_modules/shell-exec/README.md +60 -0
  329. package/server/whatsapp-rpc/node_modules/shell-exec/index.js +47 -0
  330. package/server/whatsapp-rpc/node_modules/shell-exec/package.json +29 -0
  331. package/server/whatsapp-rpc/node_modules/signal-exit/LICENSE.txt +16 -0
  332. package/server/whatsapp-rpc/node_modules/signal-exit/README.md +74 -0
  333. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts +12 -0
  334. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts.map +1 -0
  335. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js +10 -0
  336. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js.map +1 -0
  337. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts +48 -0
  338. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts.map +1 -0
  339. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js +279 -0
  340. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js.map +1 -0
  341. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/package.json +3 -0
  342. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts +29 -0
  343. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts.map +1 -0
  344. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js +42 -0
  345. package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js.map +1 -0
  346. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts +12 -0
  347. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts.map +1 -0
  348. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js +4 -0
  349. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js.map +1 -0
  350. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts +48 -0
  351. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts.map +1 -0
  352. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js +275 -0
  353. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js.map +1 -0
  354. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/package.json +3 -0
  355. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts +29 -0
  356. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts.map +1 -0
  357. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js +39 -0
  358. package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js.map +1 -0
  359. package/server/whatsapp-rpc/node_modules/signal-exit/package.json +106 -0
  360. package/server/whatsapp-rpc/node_modules/strip-final-newline/index.js +14 -0
  361. package/server/whatsapp-rpc/node_modules/strip-final-newline/license +9 -0
  362. package/server/whatsapp-rpc/node_modules/strip-final-newline/package.json +43 -0
  363. package/server/whatsapp-rpc/node_modules/strip-final-newline/readme.md +35 -0
  364. package/server/whatsapp-rpc/node_modules/which/CHANGELOG.md +166 -0
  365. package/server/whatsapp-rpc/node_modules/which/LICENSE +15 -0
  366. package/server/whatsapp-rpc/node_modules/which/README.md +54 -0
  367. package/server/whatsapp-rpc/node_modules/which/bin/node-which +52 -0
  368. package/server/whatsapp-rpc/node_modules/which/package.json +43 -0
  369. package/server/whatsapp-rpc/node_modules/which/which.js +125 -0
  370. package/server/whatsapp-rpc/package-lock.json +272 -0
  371. package/server/whatsapp-rpc/package.json +30 -30
  372. package/server/whatsapp-rpc/schema.json +1294 -1294
  373. package/server/whatsapp-rpc/scripts/clean.cjs +66 -66
  374. package/server/whatsapp-rpc/scripts/cli.js +162 -162
  375. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -166
  376. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -15
  377. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -4
  378. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -427
  379. package/server/whatsapp-rpc/web/app.py +609 -609
  380. package/server/whatsapp-rpc/web/requirements.txt +6 -6
  381. package/server/whatsapp-rpc/web/rpc_client.py +427 -427
  382. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -59
  383. package/server/whatsapp-rpc/web/templates/base.html +149 -149
  384. package/server/whatsapp-rpc/web/templates/contacts.html +240 -240
  385. package/server/whatsapp-rpc/web/templates/dashboard.html +319 -319
  386. package/server/whatsapp-rpc/web/templates/groups.html +328 -328
  387. package/server/whatsapp-rpc/web/templates/messages.html +465 -465
  388. package/server/whatsapp-rpc/web/templates/messaging.html +680 -680
  389. package/server/whatsapp-rpc/web/templates/send.html +258 -258
  390. package/server/whatsapp-rpc/web/templates/settings.html +459 -459
  391. package/client/src/components/ui/AndroidSettingsPanel.tsx +0 -401
  392. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +0 -345
  393. package/client/src/nodeDefinitions/androidDeviceNodes.ts +0 -140
  394. package/docker-compose.prod.yml +0 -107
  395. package/docker-compose.yml +0 -104
  396. package/docs-MachinaOs/README.md +0 -85
  397. package/docs-MachinaOs/deployment/docker.mdx +0 -228
  398. package/docs-MachinaOs/deployment/production.mdx +0 -345
  399. package/docs-MachinaOs/docs.json +0 -75
  400. package/docs-MachinaOs/faq.mdx +0 -309
  401. package/docs-MachinaOs/favicon.svg +0 -5
  402. package/docs-MachinaOs/installation.mdx +0 -160
  403. package/docs-MachinaOs/introduction.mdx +0 -114
  404. package/docs-MachinaOs/logo/dark.svg +0 -6
  405. package/docs-MachinaOs/logo/light.svg +0 -6
  406. package/docs-MachinaOs/nodes/ai-agent.mdx +0 -216
  407. package/docs-MachinaOs/nodes/ai-models.mdx +0 -240
  408. package/docs-MachinaOs/nodes/android.mdx +0 -411
  409. package/docs-MachinaOs/nodes/overview.mdx +0 -181
  410. package/docs-MachinaOs/nodes/schedulers.mdx +0 -316
  411. package/docs-MachinaOs/nodes/webhooks.mdx +0 -330
  412. package/docs-MachinaOs/nodes/whatsapp.mdx +0 -305
  413. package/docs-MachinaOs/quickstart.mdx +0 -119
  414. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +0 -177
  415. package/docs-MachinaOs/tutorials/android-automation.mdx +0 -242
  416. package/docs-MachinaOs/tutorials/first-workflow.mdx +0 -134
  417. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +0 -185
  418. package/nul +0 -0
  419. package/scripts/check-ports.ps1 +0 -33
  420. 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