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.
- package/.env.template +71 -71
- package/LICENSE +21 -21
- package/README.md +145 -87
- package/bin/cli.js +62 -106
- package/client/.dockerignore +45 -45
- package/client/Dockerfile +68 -68
- package/client/dist/assets/index-DFSC53FP.css +1 -0
- package/client/dist/assets/index-fJ-1gTf5.js +613 -0
- package/client/dist/index.html +14 -0
- package/client/eslint.config.js +34 -16
- package/client/nginx.conf +66 -66
- package/client/package.json +61 -48
- package/client/src/App.tsx +27 -27
- package/client/src/Dashboard.tsx +1200 -1172
- package/client/src/ParameterPanel.tsx +302 -300
- package/client/src/components/AIAgentNode.tsx +315 -321
- package/client/src/components/APIKeyValidator.tsx +117 -117
- package/client/src/components/ClaudeChatModelNode.tsx +17 -17
- package/client/src/components/CredentialsModal.tsx +1200 -306
- package/client/src/components/GeminiChatModelNode.tsx +17 -17
- package/client/src/components/GenericNode.tsx +356 -356
- package/client/src/components/LocationParameterPanel.tsx +153 -153
- package/client/src/components/ModelNode.tsx +285 -285
- package/client/src/components/OpenAIChatModelNode.tsx +17 -17
- package/client/src/components/OutputPanel.tsx +470 -470
- package/client/src/components/ParameterRenderer.tsx +1873 -1873
- package/client/src/components/SkillEditorModal.tsx +3 -3
- package/client/src/components/SquareNode.tsx +812 -796
- package/client/src/components/ToolkitNode.tsx +365 -365
- package/client/src/components/auth/LoginPage.tsx +247 -247
- package/client/src/components/auth/ProtectedRoute.tsx +59 -59
- package/client/src/components/base/BaseChatModelNode.tsx +270 -270
- package/client/src/components/icons/AIProviderIcons.tsx +50 -50
- package/client/src/components/maps/GoogleMapsPicker.tsx +136 -136
- package/client/src/components/maps/MapsPreviewPanel.tsx +109 -109
- package/client/src/components/maps/index.ts +25 -25
- package/client/src/components/parameterPanel/InputSection.tsx +1094 -1094
- package/client/src/components/parameterPanel/LocationPanelLayout.tsx +64 -64
- package/client/src/components/parameterPanel/MapsSection.tsx +91 -91
- package/client/src/components/parameterPanel/MiddleSection.tsx +867 -571
- package/client/src/components/parameterPanel/OutputSection.tsx +80 -80
- package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +81 -81
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -436
- package/client/src/components/parameterPanel/index.ts +41 -41
- package/client/src/components/shared/DataPanel.tsx +142 -142
- package/client/src/components/shared/JSONTreeRenderer.tsx +105 -105
- package/client/src/components/ui/AIResultModal.tsx +203 -203
- package/client/src/components/ui/ApiKeyInput.tsx +93 -0
- package/client/src/components/ui/CodeEditor.tsx +81 -81
- package/client/src/components/ui/CollapsibleSection.tsx +87 -87
- package/client/src/components/ui/ComponentItem.tsx +153 -153
- package/client/src/components/ui/ComponentPalette.tsx +320 -320
- package/client/src/components/ui/ConsolePanel.tsx +151 -43
- package/client/src/components/ui/ErrorBoundary.tsx +195 -195
- package/client/src/components/ui/InputNodesPanel.tsx +203 -203
- package/client/src/components/ui/MapSelector.tsx +313 -313
- package/client/src/components/ui/Modal.tsx +151 -148
- package/client/src/components/ui/NodeOutputPanel.tsx +1150 -1150
- package/client/src/components/ui/OutputDisplayPanel.tsx +381 -381
- package/client/src/components/ui/QRCodeDisplay.tsx +182 -0
- package/client/src/components/ui/TopToolbar.tsx +736 -736
- package/client/src/components/ui/WorkflowSidebar.tsx +293 -293
- package/client/src/config/antdTheme.ts +186 -186
- package/client/src/contexts/AuthContext.tsx +221 -221
- package/client/src/contexts/ThemeContext.tsx +42 -42
- package/client/src/contexts/WebSocketContext.tsx +2144 -1971
- package/client/src/factories/baseChatModelFactory.ts +255 -255
- package/client/src/hooks/useAndroidOperations.ts +118 -164
- package/client/src/hooks/useApiKeyValidation.ts +106 -106
- package/client/src/hooks/useApiKeys.ts +238 -238
- package/client/src/hooks/useAppTheme.ts +17 -17
- package/client/src/hooks/useComponentPalette.ts +50 -50
- package/client/src/hooks/useDragAndDrop.ts +123 -123
- package/client/src/hooks/useDragVariable.ts +88 -88
- package/client/src/hooks/useExecution.ts +319 -313
- package/client/src/hooks/useParameterPanel.ts +176 -176
- package/client/src/hooks/useReactFlowNodes.ts +188 -188
- package/client/src/hooks/useToolSchema.ts +209 -209
- package/client/src/hooks/useWhatsApp.ts +196 -196
- package/client/src/hooks/useWorkflowManagement.ts +45 -45
- package/client/src/index.css +314 -314
- package/client/src/nodeDefinitions/aiAgentNodes.ts +335 -335
- package/client/src/nodeDefinitions/aiModelNodes.ts +340 -340
- package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -383
- package/client/src/nodeDefinitions/chatNodes.ts +135 -135
- package/client/src/nodeDefinitions/codeNodes.ts +54 -54
- package/client/src/nodeDefinitions/index.ts +14 -14
- package/client/src/nodeDefinitions/locationNodes.ts +462 -462
- package/client/src/nodeDefinitions/schedulerNodes.ts +220 -220
- package/client/src/nodeDefinitions/skillNodes.ts +17 -5
- package/client/src/nodeDefinitions/utilityNodes.ts +284 -284
- package/client/src/nodeDefinitions/whatsappNodes.ts +821 -865
- package/client/src/nodeDefinitions.ts +101 -103
- package/client/src/services/dynamicParameterService.ts +95 -95
- package/client/src/services/execution/aiAgentExecutionService.ts +34 -34
- package/client/src/services/executionService.ts +227 -231
- package/client/src/services/workflowApi.ts +91 -91
- package/client/src/store/useAppStore.ts +578 -581
- package/client/src/styles/theme.ts +513 -508
- package/client/src/styles/zIndex.ts +16 -16
- package/client/src/types/ComponentTypes.ts +38 -38
- package/client/src/types/INodeProperties.ts +287 -287
- package/client/src/types/NodeTypes.ts +27 -27
- package/client/src/utils/formatters.ts +32 -32
- package/client/src/utils/googleMapsLoader.ts +139 -139
- package/client/src/utils/locationUtils.ts +84 -84
- package/client/src/utils/nodeUtils.ts +30 -30
- package/client/src/utils/workflow.ts +29 -29
- package/client/src/vite-env.d.ts +12 -12
- package/client/tailwind.config.js +59 -59
- package/client/tsconfig.json +25 -25
- package/client/vite.config.js +35 -35
- package/package.json +78 -70
- package/scripts/build.js +153 -45
- package/scripts/clean.js +40 -40
- package/scripts/start.js +234 -210
- package/scripts/stop.js +301 -325
- package/server/.dockerignore +44 -44
- package/server/Dockerfile +45 -45
- package/server/constants.py +244 -249
- package/server/core/cache.py +460 -460
- package/server/core/config.py +127 -127
- package/server/core/container.py +98 -98
- package/server/core/database.py +1296 -1210
- package/server/core/logging.py +313 -313
- package/server/main.py +288 -288
- package/server/middleware/__init__.py +5 -5
- package/server/middleware/auth.py +89 -89
- package/server/models/auth.py +52 -52
- package/server/models/cache.py +24 -24
- package/server/models/database.py +235 -210
- package/server/models/nodes.py +435 -455
- package/server/pyproject.toml +75 -72
- package/server/requirements.txt +83 -83
- package/server/routers/android.py +294 -294
- package/server/routers/auth.py +203 -203
- package/server/routers/database.py +150 -150
- package/server/routers/maps.py +141 -141
- package/server/routers/nodejs_compat.py +288 -288
- package/server/routers/webhook.py +90 -90
- package/server/routers/websocket.py +2239 -2127
- package/server/routers/whatsapp.py +761 -761
- package/server/routers/workflow.py +199 -199
- package/server/services/ai.py +2444 -2414
- package/server/services/android_service.py +588 -588
- package/server/services/auth.py +130 -130
- package/server/services/chat_client.py +160 -160
- package/server/services/deployment/manager.py +706 -706
- package/server/services/event_waiter.py +675 -785
- package/server/services/execution/executor.py +1351 -1351
- package/server/services/execution/models.py +1 -1
- package/server/services/handlers/__init__.py +122 -126
- package/server/services/handlers/ai.py +390 -355
- package/server/services/handlers/android.py +69 -260
- package/server/services/handlers/code.py +278 -278
- package/server/services/handlers/http.py +193 -193
- package/server/services/handlers/tools.py +146 -32
- package/server/services/handlers/triggers.py +107 -107
- package/server/services/handlers/utility.py +822 -822
- package/server/services/handlers/whatsapp.py +423 -476
- package/server/services/maps.py +288 -288
- package/server/services/memory_store.py +103 -103
- package/server/services/node_executor.py +372 -375
- package/server/services/scheduler.py +155 -155
- package/server/services/skill_loader.py +1 -1
- package/server/services/status_broadcaster.py +834 -826
- package/server/services/temporal/__init__.py +23 -23
- package/server/services/temporal/activities.py +344 -344
- package/server/services/temporal/client.py +76 -76
- package/server/services/temporal/executor.py +147 -147
- package/server/services/temporal/worker.py +251 -251
- package/server/services/temporal/workflow.py +355 -355
- package/server/services/temporal/ws_client.py +236 -236
- package/server/services/text.py +110 -110
- package/server/services/user_auth.py +172 -172
- package/server/services/websocket_client.py +29 -29
- package/server/services/workflow.py +597 -597
- package/server/skills/android-skill/SKILL.md +4 -4
- package/server/skills/code-skill/SKILL.md +123 -89
- package/server/skills/maps-skill/SKILL.md +3 -3
- package/server/skills/memory-skill/SKILL.md +1 -1
- package/server/skills/web-search-skill/SKILL.md +154 -0
- package/server/skills/whatsapp-skill/SKILL.md +3 -3
- package/server/uv.lock +461 -100
- package/server/whatsapp-rpc/.dockerignore +30 -30
- package/server/whatsapp-rpc/Dockerfile +44 -44
- package/server/whatsapp-rpc/Dockerfile.web +17 -17
- package/server/whatsapp-rpc/README.md +139 -139
- package/server/whatsapp-rpc/bin/whatsapp-rpc-server +0 -0
- package/server/whatsapp-rpc/cli.js +95 -95
- package/server/whatsapp-rpc/configs/config.yaml +6 -6
- package/server/whatsapp-rpc/docker-compose.yml +35 -35
- package/server/whatsapp-rpc/docs/API.md +410 -410
- package/server/whatsapp-rpc/node_modules/.package-lock.json +259 -0
- package/server/whatsapp-rpc/node_modules/chalk/license +9 -0
- package/server/whatsapp-rpc/node_modules/chalk/package.json +83 -0
- package/server/whatsapp-rpc/node_modules/chalk/readme.md +297 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/index.d.ts +325 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/index.js +225 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/utilities.js +33 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/server/whatsapp-rpc/node_modules/commander/LICENSE +22 -0
- package/server/whatsapp-rpc/node_modules/commander/Readme.md +1148 -0
- package/server/whatsapp-rpc/node_modules/commander/esm.mjs +16 -0
- package/server/whatsapp-rpc/node_modules/commander/index.js +26 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/argument.js +145 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/command.js +2179 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/error.js +43 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/help.js +462 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/option.js +329 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/suggestSimilar.js +100 -0
- package/server/whatsapp-rpc/node_modules/commander/package-support.json +16 -0
- package/server/whatsapp-rpc/node_modules/commander/package.json +80 -0
- package/server/whatsapp-rpc/node_modules/commander/typings/esm.d.mts +3 -0
- package/server/whatsapp-rpc/node_modules/commander/typings/index.d.ts +884 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/LICENSE +21 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/README.md +89 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/index.js +39 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/lib/enoent.js +59 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/lib/parse.js +91 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/escape.js +47 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/readShebang.js +23 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/resolveCommand.js +52 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/package.json +73 -0
- package/server/whatsapp-rpc/node_modules/execa/index.d.ts +955 -0
- package/server/whatsapp-rpc/node_modules/execa/index.js +309 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/command.js +119 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/error.js +87 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/kill.js +102 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/pipe.js +42 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/promise.js +36 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/stdio.js +49 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/stream.js +133 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/verbose.js +19 -0
- package/server/whatsapp-rpc/node_modules/execa/license +9 -0
- package/server/whatsapp-rpc/node_modules/execa/package.json +90 -0
- package/server/whatsapp-rpc/node_modules/execa/readme.md +822 -0
- package/server/whatsapp-rpc/node_modules/get-stream/license +9 -0
- package/server/whatsapp-rpc/node_modules/get-stream/package.json +53 -0
- package/server/whatsapp-rpc/node_modules/get-stream/readme.md +291 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/array-buffer.js +84 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/array.js +32 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/buffer.js +20 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/contents.js +101 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/index.d.ts +119 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/index.js +5 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/string.js +36 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/utils.js +11 -0
- package/server/whatsapp-rpc/node_modules/get-them-args/LICENSE +21 -0
- package/server/whatsapp-rpc/node_modules/get-them-args/README.md +95 -0
- package/server/whatsapp-rpc/node_modules/get-them-args/index.js +97 -0
- package/server/whatsapp-rpc/node_modules/get-them-args/package.json +36 -0
- package/server/whatsapp-rpc/node_modules/human-signals/LICENSE +201 -0
- package/server/whatsapp-rpc/node_modules/human-signals/README.md +168 -0
- package/server/whatsapp-rpc/node_modules/human-signals/build/src/core.js +273 -0
- package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.d.ts +73 -0
- package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.js +70 -0
- package/server/whatsapp-rpc/node_modules/human-signals/build/src/realtime.js +16 -0
- package/server/whatsapp-rpc/node_modules/human-signals/build/src/signals.js +34 -0
- package/server/whatsapp-rpc/node_modules/human-signals/package.json +61 -0
- package/server/whatsapp-rpc/node_modules/is-stream/index.d.ts +81 -0
- package/server/whatsapp-rpc/node_modules/is-stream/index.js +29 -0
- package/server/whatsapp-rpc/node_modules/is-stream/license +9 -0
- package/server/whatsapp-rpc/node_modules/is-stream/package.json +44 -0
- package/server/whatsapp-rpc/node_modules/is-stream/readme.md +60 -0
- package/server/whatsapp-rpc/node_modules/isexe/LICENSE +15 -0
- package/server/whatsapp-rpc/node_modules/isexe/README.md +51 -0
- package/server/whatsapp-rpc/node_modules/isexe/index.js +57 -0
- package/server/whatsapp-rpc/node_modules/isexe/mode.js +41 -0
- package/server/whatsapp-rpc/node_modules/isexe/package.json +31 -0
- package/server/whatsapp-rpc/node_modules/isexe/test/basic.js +221 -0
- package/server/whatsapp-rpc/node_modules/isexe/windows.js +42 -0
- package/server/whatsapp-rpc/node_modules/kill-port/.editorconfig +12 -0
- package/server/whatsapp-rpc/node_modules/kill-port/.gitattributes +1 -0
- package/server/whatsapp-rpc/node_modules/kill-port/LICENSE +21 -0
- package/server/whatsapp-rpc/node_modules/kill-port/README.md +140 -0
- package/server/whatsapp-rpc/node_modules/kill-port/cli.js +25 -0
- package/server/whatsapp-rpc/node_modules/kill-port/example.js +21 -0
- package/server/whatsapp-rpc/node_modules/kill-port/index.js +46 -0
- package/server/whatsapp-rpc/node_modules/kill-port/logo.png +0 -0
- package/server/whatsapp-rpc/node_modules/kill-port/package.json +41 -0
- package/server/whatsapp-rpc/node_modules/kill-port/pnpm-lock.yaml +4606 -0
- package/server/whatsapp-rpc/node_modules/kill-port/test.js +16 -0
- package/server/whatsapp-rpc/node_modules/merge-stream/LICENSE +21 -0
- package/server/whatsapp-rpc/node_modules/merge-stream/README.md +78 -0
- package/server/whatsapp-rpc/node_modules/merge-stream/index.js +41 -0
- package/server/whatsapp-rpc/node_modules/merge-stream/package.json +19 -0
- package/server/whatsapp-rpc/node_modules/mimic-fn/index.d.ts +52 -0
- package/server/whatsapp-rpc/node_modules/mimic-fn/index.js +71 -0
- package/server/whatsapp-rpc/node_modules/mimic-fn/license +9 -0
- package/server/whatsapp-rpc/node_modules/mimic-fn/package.json +45 -0
- package/server/whatsapp-rpc/node_modules/mimic-fn/readme.md +90 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/index.d.ts +90 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/index.js +52 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/license +9 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.d.ts +31 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.js +12 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/license +9 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/package.json +41 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/readme.md +57 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/package.json +49 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/readme.md +104 -0
- package/server/whatsapp-rpc/node_modules/onetime/index.d.ts +59 -0
- package/server/whatsapp-rpc/node_modules/onetime/index.js +41 -0
- package/server/whatsapp-rpc/node_modules/onetime/license +9 -0
- package/server/whatsapp-rpc/node_modules/onetime/package.json +45 -0
- package/server/whatsapp-rpc/node_modules/onetime/readme.md +94 -0
- package/server/whatsapp-rpc/node_modules/path-key/index.d.ts +40 -0
- package/server/whatsapp-rpc/node_modules/path-key/index.js +16 -0
- package/server/whatsapp-rpc/node_modules/path-key/license +9 -0
- package/server/whatsapp-rpc/node_modules/path-key/package.json +39 -0
- package/server/whatsapp-rpc/node_modules/path-key/readme.md +61 -0
- package/server/whatsapp-rpc/node_modules/shebang-command/index.js +19 -0
- package/server/whatsapp-rpc/node_modules/shebang-command/license +9 -0
- package/server/whatsapp-rpc/node_modules/shebang-command/package.json +34 -0
- package/server/whatsapp-rpc/node_modules/shebang-command/readme.md +34 -0
- package/server/whatsapp-rpc/node_modules/shebang-regex/index.d.ts +22 -0
- package/server/whatsapp-rpc/node_modules/shebang-regex/index.js +2 -0
- package/server/whatsapp-rpc/node_modules/shebang-regex/license +9 -0
- package/server/whatsapp-rpc/node_modules/shebang-regex/package.json +35 -0
- package/server/whatsapp-rpc/node_modules/shebang-regex/readme.md +33 -0
- package/server/whatsapp-rpc/node_modules/shell-exec/LICENSE +21 -0
- package/server/whatsapp-rpc/node_modules/shell-exec/README.md +60 -0
- package/server/whatsapp-rpc/node_modules/shell-exec/index.js +47 -0
- package/server/whatsapp-rpc/node_modules/shell-exec/package.json +29 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/LICENSE.txt +16 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/README.md +74 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts +12 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js +10 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts +48 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js +279 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/package.json +3 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts +29 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js +42 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts +12 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js +4 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts +48 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js +275 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/package.json +3 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts +29 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js +39 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/package.json +106 -0
- package/server/whatsapp-rpc/node_modules/strip-final-newline/index.js +14 -0
- package/server/whatsapp-rpc/node_modules/strip-final-newline/license +9 -0
- package/server/whatsapp-rpc/node_modules/strip-final-newline/package.json +43 -0
- package/server/whatsapp-rpc/node_modules/strip-final-newline/readme.md +35 -0
- package/server/whatsapp-rpc/node_modules/which/CHANGELOG.md +166 -0
- package/server/whatsapp-rpc/node_modules/which/LICENSE +15 -0
- package/server/whatsapp-rpc/node_modules/which/README.md +54 -0
- package/server/whatsapp-rpc/node_modules/which/bin/node-which +52 -0
- package/server/whatsapp-rpc/node_modules/which/package.json +43 -0
- package/server/whatsapp-rpc/node_modules/which/which.js +125 -0
- package/server/whatsapp-rpc/package-lock.json +272 -0
- package/server/whatsapp-rpc/package.json +30 -30
- package/server/whatsapp-rpc/schema.json +1294 -1294
- package/server/whatsapp-rpc/scripts/clean.cjs +66 -66
- package/server/whatsapp-rpc/scripts/cli.js +162 -162
- package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -166
- package/server/whatsapp-rpc/src/python/pyproject.toml +15 -15
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -4
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -427
- package/server/whatsapp-rpc/web/app.py +609 -609
- package/server/whatsapp-rpc/web/requirements.txt +6 -6
- package/server/whatsapp-rpc/web/rpc_client.py +427 -427
- package/server/whatsapp-rpc/web/static/openapi.yaml +59 -59
- package/server/whatsapp-rpc/web/templates/base.html +149 -149
- package/server/whatsapp-rpc/web/templates/contacts.html +240 -240
- package/server/whatsapp-rpc/web/templates/dashboard.html +319 -319
- package/server/whatsapp-rpc/web/templates/groups.html +328 -328
- package/server/whatsapp-rpc/web/templates/messages.html +465 -465
- package/server/whatsapp-rpc/web/templates/messaging.html +680 -680
- package/server/whatsapp-rpc/web/templates/send.html +258 -258
- package/server/whatsapp-rpc/web/templates/settings.html +459 -459
- package/client/src/components/ui/AndroidSettingsPanel.tsx +0 -401
- package/client/src/components/ui/WhatsAppSettingsPanel.tsx +0 -345
- package/client/src/nodeDefinitions/androidDeviceNodes.ts +0 -140
- package/docker-compose.prod.yml +0 -107
- package/docker-compose.yml +0 -104
- package/docs-MachinaOs/README.md +0 -85
- package/docs-MachinaOs/deployment/docker.mdx +0 -228
- package/docs-MachinaOs/deployment/production.mdx +0 -345
- package/docs-MachinaOs/docs.json +0 -75
- package/docs-MachinaOs/faq.mdx +0 -309
- package/docs-MachinaOs/favicon.svg +0 -5
- package/docs-MachinaOs/installation.mdx +0 -160
- package/docs-MachinaOs/introduction.mdx +0 -114
- package/docs-MachinaOs/logo/dark.svg +0 -6
- package/docs-MachinaOs/logo/light.svg +0 -6
- package/docs-MachinaOs/nodes/ai-agent.mdx +0 -216
- package/docs-MachinaOs/nodes/ai-models.mdx +0 -240
- package/docs-MachinaOs/nodes/android.mdx +0 -411
- package/docs-MachinaOs/nodes/overview.mdx +0 -181
- package/docs-MachinaOs/nodes/schedulers.mdx +0 -316
- package/docs-MachinaOs/nodes/webhooks.mdx +0 -330
- package/docs-MachinaOs/nodes/whatsapp.mdx +0 -305
- package/docs-MachinaOs/quickstart.mdx +0 -119
- package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +0 -177
- package/docs-MachinaOs/tutorials/android-automation.mdx +0 -242
- package/docs-MachinaOs/tutorials/first-workflow.mdx +0 -134
- package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +0 -185
- package/nul +0 -0
- package/scripts/check-ports.ps1 +0 -33
- package/scripts/kill-port.ps1 +0 -154
package/server/core/cache.py
CHANGED
|
@@ -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
|