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/database.py
CHANGED
|
@@ -1,1211 +1,1297 @@
|
|
|
1
|
-
"""Modern async database service with SQLModel and SQLAlchemy 2.0."""
|
|
2
|
-
|
|
3
|
-
from datetime import datetime, timedelta, timezone
|
|
4
|
-
from typing import Dict, Any, List, Optional
|
|
5
|
-
from sqlmodel import SQLModel, select, Session
|
|
6
|
-
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
|
7
|
-
from sqlalchemy.exc import IntegrityError
|
|
8
|
-
from contextlib import asynccontextmanager
|
|
9
|
-
|
|
10
|
-
from core.config import Settings
|
|
11
|
-
from models.database import NodeParameter, Workflow, Execution, APIKey, APIKeyValidation, NodeOutput, ConversationMessage, ToolSchema, UserSkill, ChatMessage
|
|
12
|
-
from models.cache import CacheEntry # SQLite-backed cache for Redis alternative
|
|
13
|
-
from models.auth import User # Import User model to ensure table creation
|
|
14
|
-
from core.logging import get_logger
|
|
15
|
-
|
|
16
|
-
logger = get_logger(__name__)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class Database:
|
|
20
|
-
"""Async database service with SQLModel."""
|
|
21
|
-
|
|
22
|
-
def __init__(self, settings: Settings):
|
|
23
|
-
self.settings = settings
|
|
24
|
-
self.engine = None
|
|
25
|
-
self.async_session = None
|
|
26
|
-
|
|
27
|
-
async def startup(self):
|
|
28
|
-
"""Initialize database connection and create tables."""
|
|
29
|
-
try:
|
|
30
|
-
# Disable verbose database and asyncio logging
|
|
31
|
-
import logging
|
|
32
|
-
logging.getLogger("aiosqlite").setLevel(logging.WARNING)
|
|
33
|
-
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
|
34
|
-
logging.getLogger("sqlalchemy.dialects").setLevel(logging.WARNING)
|
|
35
|
-
logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING)
|
|
36
|
-
|
|
37
|
-
# Create async engine
|
|
38
|
-
self.engine = create_async_engine(
|
|
39
|
-
self.settings.database_url,
|
|
40
|
-
echo=self.settings.database_echo,
|
|
41
|
-
pool_size=self.settings.database_pool_size,
|
|
42
|
-
max_overflow=self.settings.database_max_overflow,
|
|
43
|
-
future=True
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
# Create session factory
|
|
47
|
-
self.async_session = async_sessionmaker(
|
|
48
|
-
bind=self.engine,
|
|
49
|
-
class_=AsyncSession,
|
|
50
|
-
expire_on_commit=False
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
# Create tables
|
|
54
|
-
async with self.engine.begin() as conn:
|
|
55
|
-
await conn.run_sync(SQLModel.metadata.create_all)
|
|
56
|
-
|
|
57
|
-
logger.info("Database initialized successfully")
|
|
58
|
-
|
|
59
|
-
except Exception as e:
|
|
60
|
-
logger.error("Database startup failed", error=str(e))
|
|
61
|
-
raise
|
|
62
|
-
|
|
63
|
-
async def shutdown(self):
|
|
64
|
-
"""Close database connections."""
|
|
65
|
-
if self.engine:
|
|
66
|
-
await self.engine.dispose()
|
|
67
|
-
logger.info("Database connections closed")
|
|
68
|
-
|
|
69
|
-
@asynccontextmanager
|
|
70
|
-
async def get_session(self):
|
|
71
|
-
"""Get async database session."""
|
|
72
|
-
if not self.async_session:
|
|
73
|
-
raise RuntimeError("Database not initialized")
|
|
74
|
-
|
|
75
|
-
async with self.async_session() as session:
|
|
76
|
-
try:
|
|
77
|
-
yield session
|
|
78
|
-
except Exception:
|
|
79
|
-
await session.rollback()
|
|
80
|
-
raise
|
|
81
|
-
finally:
|
|
82
|
-
await session.close()
|
|
83
|
-
|
|
84
|
-
# ============================================================================
|
|
85
|
-
# Node Parameters
|
|
86
|
-
# ============================================================================
|
|
87
|
-
|
|
88
|
-
async def save_node_parameters(self, node_id: str, parameters: Dict[str, Any]) -> bool:
|
|
89
|
-
"""Save or update node parameters."""
|
|
90
|
-
try:
|
|
91
|
-
async with self.get_session() as session:
|
|
92
|
-
# Try to get existing parameter
|
|
93
|
-
stmt = select(NodeParameter).where(NodeParameter.node_id == node_id)
|
|
94
|
-
result = await session.execute(stmt)
|
|
95
|
-
existing = result.scalar_one_or_none()
|
|
96
|
-
|
|
97
|
-
if existing:
|
|
98
|
-
existing.parameters = parameters
|
|
99
|
-
else:
|
|
100
|
-
existing = NodeParameter(
|
|
101
|
-
node_id=node_id,
|
|
102
|
-
parameters=parameters
|
|
103
|
-
)
|
|
104
|
-
session.add(existing)
|
|
105
|
-
|
|
106
|
-
await session.commit()
|
|
107
|
-
return True
|
|
108
|
-
|
|
109
|
-
except Exception as e:
|
|
110
|
-
logger.error("Failed to save node parameters", node_id=node_id, error=str(e))
|
|
111
|
-
return False
|
|
112
|
-
|
|
113
|
-
async def get_node_parameters(self, node_id: str) -> Optional[Dict[str, Any]]:
|
|
114
|
-
"""Get node parameters."""
|
|
115
|
-
try:
|
|
116
|
-
async with self.get_session() as session:
|
|
117
|
-
stmt = select(NodeParameter).where(NodeParameter.node_id == node_id)
|
|
118
|
-
result = await session.execute(stmt)
|
|
119
|
-
parameter = result.scalar_one_or_none()
|
|
120
|
-
|
|
121
|
-
return parameter.parameters if parameter else None
|
|
122
|
-
|
|
123
|
-
except Exception as e:
|
|
124
|
-
logger.error("Failed to get node parameters", node_id=node_id, error=str(e))
|
|
125
|
-
return None
|
|
126
|
-
|
|
127
|
-
async def delete_node_parameters(self, node_id: str) -> bool:
|
|
128
|
-
"""Delete node parameters."""
|
|
129
|
-
try:
|
|
130
|
-
async with self.get_session() as session:
|
|
131
|
-
stmt = select(NodeParameter).where(NodeParameter.node_id == node_id)
|
|
132
|
-
result = await session.execute(stmt)
|
|
133
|
-
parameter = result.scalar_one_or_none()
|
|
134
|
-
|
|
135
|
-
if parameter:
|
|
136
|
-
await session.delete(parameter)
|
|
137
|
-
await session.commit()
|
|
138
|
-
|
|
139
|
-
return True
|
|
140
|
-
|
|
141
|
-
except Exception as e:
|
|
142
|
-
logger.error("Failed to delete node parameters", node_id=node_id, error=str(e))
|
|
143
|
-
return False
|
|
144
|
-
|
|
145
|
-
# ============================================================================
|
|
146
|
-
# Workflows
|
|
147
|
-
# ============================================================================
|
|
148
|
-
|
|
149
|
-
async def save_workflow(self, workflow_id: str, name: str, data: Dict[str, Any],
|
|
150
|
-
description: Optional[str] = None) -> bool:
|
|
151
|
-
"""Save or update workflow."""
|
|
152
|
-
try:
|
|
153
|
-
async with self.get_session() as session:
|
|
154
|
-
stmt = select(Workflow).where(Workflow.id == workflow_id)
|
|
155
|
-
result = await session.execute(stmt)
|
|
156
|
-
existing = result.scalar_one_or_none()
|
|
157
|
-
|
|
158
|
-
if existing:
|
|
159
|
-
existing.name = name
|
|
160
|
-
existing.description = description
|
|
161
|
-
existing.data = data
|
|
162
|
-
else:
|
|
163
|
-
existing = Workflow(
|
|
164
|
-
id=workflow_id,
|
|
165
|
-
name=name,
|
|
166
|
-
description=description,
|
|
167
|
-
data=data
|
|
168
|
-
)
|
|
169
|
-
session.add(existing)
|
|
170
|
-
|
|
171
|
-
await session.commit()
|
|
172
|
-
return True
|
|
173
|
-
|
|
174
|
-
except Exception as e:
|
|
175
|
-
logger.error("Failed to save workflow", workflow_id=workflow_id, error=str(e))
|
|
176
|
-
return False
|
|
177
|
-
|
|
178
|
-
async def get_workflow(self, workflow_id: str) -> Optional[Workflow]:
|
|
179
|
-
"""Get workflow by ID."""
|
|
180
|
-
try:
|
|
181
|
-
async with self.get_session() as session:
|
|
182
|
-
stmt = select(Workflow).where(Workflow.id == workflow_id)
|
|
183
|
-
result = await session.execute(stmt)
|
|
184
|
-
return result.scalar_one_or_none()
|
|
185
|
-
|
|
186
|
-
except Exception as e:
|
|
187
|
-
logger.error("Failed to get workflow", workflow_id=workflow_id, error=str(e))
|
|
188
|
-
return None
|
|
189
|
-
|
|
190
|
-
async def get_all_workflows(self) -> List[Workflow]:
|
|
191
|
-
"""Get all workflows."""
|
|
192
|
-
try:
|
|
193
|
-
async with self.get_session() as session:
|
|
194
|
-
stmt = select(Workflow).order_by(Workflow.updated_at.desc())
|
|
195
|
-
result = await session.execute(stmt)
|
|
196
|
-
return result.scalars().all()
|
|
197
|
-
|
|
198
|
-
except Exception as e:
|
|
199
|
-
logger.error("Failed to get all workflows", error=str(e))
|
|
200
|
-
return []
|
|
201
|
-
|
|
202
|
-
async def delete_workflow(self, workflow_id: str) -> bool:
|
|
203
|
-
"""Delete workflow."""
|
|
204
|
-
try:
|
|
205
|
-
async with self.get_session() as session:
|
|
206
|
-
stmt = select(Workflow).where(Workflow.id == workflow_id)
|
|
207
|
-
result = await session.execute(stmt)
|
|
208
|
-
workflow = result.scalar_one_or_none()
|
|
209
|
-
|
|
210
|
-
if workflow:
|
|
211
|
-
await session.delete(workflow)
|
|
212
|
-
await session.commit()
|
|
213
|
-
|
|
214
|
-
return True
|
|
215
|
-
|
|
216
|
-
except Exception as e:
|
|
217
|
-
logger.error("Failed to delete workflow", workflow_id=workflow_id, error=str(e))
|
|
218
|
-
return False
|
|
219
|
-
|
|
220
|
-
# ============================================================================
|
|
221
|
-
# Executions
|
|
222
|
-
# ============================================================================
|
|
223
|
-
|
|
224
|
-
async def save_execution(self, execution_id: str, workflow_id: str, node_id: str,
|
|
225
|
-
status: str, result: Optional[Dict[str, Any]] = None,
|
|
226
|
-
error: Optional[str] = None, execution_time: Optional[float] = None) -> bool:
|
|
227
|
-
"""Save execution result."""
|
|
228
|
-
try:
|
|
229
|
-
async with self.get_session() as session:
|
|
230
|
-
execution = Execution(
|
|
231
|
-
id=execution_id,
|
|
232
|
-
workflow_id=workflow_id,
|
|
233
|
-
node_id=node_id,
|
|
234
|
-
status=status,
|
|
235
|
-
result=result,
|
|
236
|
-
error=error,
|
|
237
|
-
execution_time=execution_time
|
|
238
|
-
)
|
|
239
|
-
session.add(execution)
|
|
240
|
-
await session.commit()
|
|
241
|
-
return True
|
|
242
|
-
|
|
243
|
-
except Exception as e:
|
|
244
|
-
logger.error("Failed to save execution", execution_id=execution_id, error=str(e))
|
|
245
|
-
return False
|
|
246
|
-
|
|
247
|
-
async def get_execution(self, execution_id: str) -> Optional[Execution]:
|
|
248
|
-
"""Get execution by ID."""
|
|
249
|
-
try:
|
|
250
|
-
async with self.get_session() as session:
|
|
251
|
-
stmt = select(Execution).where(Execution.id == execution_id)
|
|
252
|
-
result = await session.execute(stmt)
|
|
253
|
-
return result.scalar_one_or_none()
|
|
254
|
-
|
|
255
|
-
except Exception as e:
|
|
256
|
-
logger.error("Failed to get execution", execution_id=execution_id, error=str(e))
|
|
257
|
-
return None
|
|
258
|
-
|
|
259
|
-
# ============================================================================
|
|
260
|
-
# API Keys
|
|
261
|
-
# ============================================================================
|
|
262
|
-
|
|
263
|
-
async def save_api_key(self, key_id: str, provider: str, session_id: str,
|
|
264
|
-
key_encrypted: str, key_hash: str,
|
|
265
|
-
models: Optional[List[str]] = None) -> bool:
|
|
266
|
-
"""Save encrypted API key."""
|
|
267
|
-
logger.info(f"Database save_api_key called with key_id: {key_id}, provider: {provider}")
|
|
268
|
-
|
|
269
|
-
try:
|
|
270
|
-
async with self.get_session() as session:
|
|
271
|
-
api_key = APIKey(
|
|
272
|
-
id=key_id,
|
|
273
|
-
provider=provider,
|
|
274
|
-
session_id=session_id,
|
|
275
|
-
key_encrypted=key_encrypted,
|
|
276
|
-
key_hash=key_hash,
|
|
277
|
-
models={"models": models} if models else None,
|
|
278
|
-
last_validated=datetime.now(timezone.utc)
|
|
279
|
-
)
|
|
280
|
-
session.add(api_key)
|
|
281
|
-
await session.commit()
|
|
282
|
-
logger.info(f"Successfully saved new API key: {key_id}")
|
|
283
|
-
return True
|
|
284
|
-
|
|
285
|
-
except IntegrityError as e:
|
|
286
|
-
logger.info(f"API key {key_id} already exists, attempting update. Error: {str(e)}")
|
|
287
|
-
# Key already exists, update it
|
|
288
|
-
try:
|
|
289
|
-
async with self.get_session() as session:
|
|
290
|
-
stmt = select(APIKey).where(APIKey.id == key_id)
|
|
291
|
-
result = await session.execute(stmt)
|
|
292
|
-
existing = result.scalar_one_or_none()
|
|
293
|
-
|
|
294
|
-
if existing:
|
|
295
|
-
logger.info(f"Found existing API key {key_id}, updating...")
|
|
296
|
-
existing.key_encrypted = key_encrypted
|
|
297
|
-
existing.key_hash = key_hash
|
|
298
|
-
existing.models = {"models": models} if models else None
|
|
299
|
-
existing.last_validated = datetime.now(timezone.utc)
|
|
300
|
-
await session.commit()
|
|
301
|
-
logger.info(f"Successfully updated API key: {key_id}")
|
|
302
|
-
return True
|
|
303
|
-
else:
|
|
304
|
-
logger.error(f"Could not find existing API key {key_id} for update")
|
|
305
|
-
return False
|
|
306
|
-
except Exception as update_e:
|
|
307
|
-
logger.error(f"Failed to update API key {key_id}", error=str(update_e))
|
|
308
|
-
return False
|
|
309
|
-
|
|
310
|
-
except Exception as e:
|
|
311
|
-
logger.error("Failed to save API key", provider=provider, error=str(e))
|
|
312
|
-
import traceback
|
|
313
|
-
logger.error("Full traceback", traceback=traceback.format_exc())
|
|
314
|
-
return False
|
|
315
|
-
|
|
316
|
-
async def get_api_key(self, key_id: str) -> Optional[APIKey]:
|
|
317
|
-
"""Get API key by ID."""
|
|
318
|
-
try:
|
|
319
|
-
async with self.get_session() as session:
|
|
320
|
-
stmt = select(APIKey).where(APIKey.id == key_id)
|
|
321
|
-
result = await session.execute(stmt)
|
|
322
|
-
return result.scalar_one_or_none()
|
|
323
|
-
|
|
324
|
-
except Exception as e:
|
|
325
|
-
logger.error("Failed to get API key", key_id=key_id, error=str(e))
|
|
326
|
-
return None
|
|
327
|
-
|
|
328
|
-
async def get_api_key_by_provider(self, provider: str, session_id: str = "default") -> Optional[APIKey]:
|
|
329
|
-
"""Get API key by provider and session."""
|
|
330
|
-
try:
|
|
331
|
-
async with self.get_session() as session:
|
|
332
|
-
stmt = select(APIKey).where(
|
|
333
|
-
APIKey.provider == provider,
|
|
334
|
-
APIKey.session_id == session_id,
|
|
335
|
-
APIKey.is_valid == True
|
|
336
|
-
)
|
|
337
|
-
result = await session.execute(stmt)
|
|
338
|
-
return result.scalar_one_or_none()
|
|
339
|
-
|
|
340
|
-
except Exception as e:
|
|
341
|
-
logger.error("Failed to get API key by provider", provider=provider, error=str(e))
|
|
342
|
-
return None
|
|
343
|
-
|
|
344
|
-
async def delete_api_key(self, provider: str, session_id: str = "default") -> bool:
|
|
345
|
-
"""Delete API key."""
|
|
346
|
-
try:
|
|
347
|
-
async with self.get_session() as session:
|
|
348
|
-
stmt = select(APIKey).where(
|
|
349
|
-
APIKey.provider == provider,
|
|
350
|
-
APIKey.session_id == session_id
|
|
351
|
-
)
|
|
352
|
-
result = await session.execute(stmt)
|
|
353
|
-
api_key = result.scalar_one_or_none()
|
|
354
|
-
|
|
355
|
-
if api_key:
|
|
356
|
-
await session.delete(api_key)
|
|
357
|
-
await session.commit()
|
|
358
|
-
logger.debug("API key deleted", provider=provider, session_id=session_id)
|
|
359
|
-
|
|
360
|
-
return True
|
|
361
|
-
|
|
362
|
-
except Exception as e:
|
|
363
|
-
logger.error("Failed to delete API key", provider=provider, error=str(e))
|
|
364
|
-
return False
|
|
365
|
-
|
|
366
|
-
# ============================================================================
|
|
367
|
-
# API Key Validation Cache
|
|
368
|
-
# ============================================================================
|
|
369
|
-
|
|
370
|
-
async def save_api_key_validation(self, key_hash: str) -> bool:
|
|
371
|
-
"""Save API key validation status."""
|
|
372
|
-
try:
|
|
373
|
-
async with self.get_session() as session:
|
|
374
|
-
validation = APIKeyValidation(
|
|
375
|
-
key_hash=key_hash,
|
|
376
|
-
validated=True
|
|
377
|
-
)
|
|
378
|
-
session.add(validation)
|
|
379
|
-
await session.commit()
|
|
380
|
-
return True
|
|
381
|
-
|
|
382
|
-
except IntegrityError:
|
|
383
|
-
# Already exists, update timestamp
|
|
384
|
-
async with self.get_session() as session:
|
|
385
|
-
stmt = select(APIKeyValidation).where(APIKeyValidation.key_hash == key_hash)
|
|
386
|
-
result = await session.execute(stmt)
|
|
387
|
-
existing = result.scalar_one_or_none()
|
|
388
|
-
|
|
389
|
-
if existing:
|
|
390
|
-
existing.timestamp = datetime.now(timezone.utc)
|
|
391
|
-
await session.commit()
|
|
392
|
-
return True
|
|
393
|
-
return False
|
|
394
|
-
|
|
395
|
-
except Exception as e:
|
|
396
|
-
logger.error("Failed to save API key validation", key_hash=key_hash, error=str(e))
|
|
397
|
-
return False
|
|
398
|
-
|
|
399
|
-
async def is_api_key_validated(self, key_hash: str) -> bool:
|
|
400
|
-
"""Check if API key is validated."""
|
|
401
|
-
try:
|
|
402
|
-
async with self.get_session() as session:
|
|
403
|
-
stmt = select(APIKeyValidation).where(APIKeyValidation.key_hash == key_hash)
|
|
404
|
-
result = await session.execute(stmt)
|
|
405
|
-
validation = result.scalar_one_or_none()
|
|
406
|
-
return validation is not None and validation.validated
|
|
407
|
-
|
|
408
|
-
except Exception as e:
|
|
409
|
-
logger.error("Failed to check API key validation", key_hash=key_hash, error=str(e))
|
|
410
|
-
return False
|
|
411
|
-
|
|
412
|
-
# ============================================================================
|
|
413
|
-
# Node Outputs
|
|
414
|
-
# ============================================================================
|
|
415
|
-
|
|
416
|
-
async def save_node_output(self, node_id: str, session_id: str, output_name: str,
|
|
417
|
-
data: Dict[str, Any]) -> bool:
|
|
418
|
-
"""Save or update node output."""
|
|
419
|
-
try:
|
|
420
|
-
async with self.get_session() as session:
|
|
421
|
-
# Try to get existing output
|
|
422
|
-
stmt = select(NodeOutput).where(
|
|
423
|
-
NodeOutput.node_id == node_id,
|
|
424
|
-
NodeOutput.session_id == session_id,
|
|
425
|
-
NodeOutput.output_name == output_name
|
|
426
|
-
)
|
|
427
|
-
result = await session.execute(stmt)
|
|
428
|
-
existing = result.scalar_one_or_none()
|
|
429
|
-
|
|
430
|
-
action = "updated"
|
|
431
|
-
if existing:
|
|
432
|
-
existing.data = data
|
|
433
|
-
else:
|
|
434
|
-
action = "inserted"
|
|
435
|
-
existing = NodeOutput(
|
|
436
|
-
node_id=node_id,
|
|
437
|
-
session_id=session_id,
|
|
438
|
-
output_name=output_name,
|
|
439
|
-
data=data
|
|
440
|
-
)
|
|
441
|
-
session.add(existing)
|
|
442
|
-
|
|
443
|
-
await session.commit()
|
|
444
|
-
logger.info("[DB] Node output saved", action=action, node_id=node_id, session_id=session_id, output_name=output_name)
|
|
445
|
-
return True
|
|
446
|
-
|
|
447
|
-
except Exception as e:
|
|
448
|
-
logger.error("Failed to save node output", node_id=node_id, error=str(e))
|
|
449
|
-
import traceback
|
|
450
|
-
traceback.print_exc()
|
|
451
|
-
return False
|
|
452
|
-
|
|
453
|
-
async def get_node_output(self, node_id: str, session_id: str = "default",
|
|
454
|
-
output_name: str = "output_0") -> Optional[Dict[str, Any]]:
|
|
455
|
-
"""Get node output data."""
|
|
456
|
-
try:
|
|
457
|
-
async with self.get_session() as session:
|
|
458
|
-
stmt = select(NodeOutput).where(
|
|
459
|
-
NodeOutput.node_id == node_id,
|
|
460
|
-
NodeOutput.session_id == session_id,
|
|
461
|
-
NodeOutput.output_name == output_name
|
|
462
|
-
)
|
|
463
|
-
result = await session.execute(stmt)
|
|
464
|
-
output = result.scalar_one_or_none()
|
|
465
|
-
|
|
466
|
-
return output.data if output else None
|
|
467
|
-
|
|
468
|
-
except Exception as e:
|
|
469
|
-
logger.error("Failed to get node output", node_id=node_id, error=str(e))
|
|
470
|
-
return None
|
|
471
|
-
|
|
472
|
-
async def delete_node_output(self, node_id: str) -> int:
|
|
473
|
-
"""Delete all outputs for a node (any session). Returns count deleted."""
|
|
474
|
-
try:
|
|
475
|
-
async with self.get_session() as session:
|
|
476
|
-
stmt = select(NodeOutput).where(NodeOutput.node_id == node_id)
|
|
477
|
-
result = await session.execute(stmt)
|
|
478
|
-
outputs = result.scalars().all()
|
|
479
|
-
|
|
480
|
-
count = len(outputs)
|
|
481
|
-
for output in outputs:
|
|
482
|
-
await session.delete(output)
|
|
483
|
-
|
|
484
|
-
await session.commit()
|
|
485
|
-
logger.info("Deleted node outputs", node_id=node_id, count=count)
|
|
486
|
-
return count
|
|
487
|
-
|
|
488
|
-
except Exception as e:
|
|
489
|
-
logger.error("Failed to delete node output", node_id=node_id, error=str(e))
|
|
490
|
-
return 0
|
|
491
|
-
|
|
492
|
-
async def clear_session_outputs(self, session_id: str = "default") -> int:
|
|
493
|
-
"""Clear all outputs for a session. Returns count deleted."""
|
|
494
|
-
try:
|
|
495
|
-
async with self.get_session() as session:
|
|
496
|
-
stmt = select(NodeOutput).where(NodeOutput.session_id == session_id)
|
|
497
|
-
result = await session.execute(stmt)
|
|
498
|
-
outputs = result.scalars().all()
|
|
499
|
-
|
|
500
|
-
count = len(outputs)
|
|
501
|
-
for output in outputs:
|
|
502
|
-
await session.delete(output)
|
|
503
|
-
|
|
504
|
-
await session.commit()
|
|
505
|
-
logger.info("Cleared session outputs", session_id=session_id, count=count)
|
|
506
|
-
return count
|
|
507
|
-
|
|
508
|
-
except Exception as e:
|
|
509
|
-
logger.error("Failed to clear session outputs", session_id=session_id, error=str(e))
|
|
510
|
-
return 0
|
|
511
|
-
|
|
512
|
-
# ============================================================================
|
|
513
|
-
# Conversation Messages (AI Memory)
|
|
514
|
-
# ============================================================================
|
|
515
|
-
|
|
516
|
-
async def add_conversation_message(self, session_id: str, role: str, content: str) -> bool:
|
|
517
|
-
"""Add a message to conversation history."""
|
|
518
|
-
try:
|
|
519
|
-
async with self.get_session() as session:
|
|
520
|
-
message = ConversationMessage(
|
|
521
|
-
session_id=session_id,
|
|
522
|
-
role=role,
|
|
523
|
-
content=content
|
|
524
|
-
)
|
|
525
|
-
session.add(message)
|
|
526
|
-
await session.commit()
|
|
527
|
-
logger.info(f"[Memory] Added {role} message to session '{session_id}'")
|
|
528
|
-
return True
|
|
529
|
-
|
|
530
|
-
except Exception as e:
|
|
531
|
-
logger.error("Failed to add conversation message", session_id=session_id, error=str(e))
|
|
532
|
-
return False
|
|
533
|
-
|
|
534
|
-
async def get_conversation_messages(self, session_id: str, window_size: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
535
|
-
"""Get conversation messages, optionally limited to last N."""
|
|
536
|
-
try:
|
|
537
|
-
async with self.get_session() as session:
|
|
538
|
-
stmt = select(ConversationMessage).where(
|
|
539
|
-
ConversationMessage.session_id == session_id
|
|
540
|
-
).order_by(ConversationMessage.created_at.asc())
|
|
541
|
-
|
|
542
|
-
result = await session.execute(stmt)
|
|
543
|
-
messages = result.scalars().all()
|
|
544
|
-
|
|
545
|
-
# Apply window limit if specified
|
|
546
|
-
if window_size and window_size > 0:
|
|
547
|
-
messages = messages[-window_size:]
|
|
548
|
-
|
|
549
|
-
return [
|
|
550
|
-
{
|
|
551
|
-
"role": m.role,
|
|
552
|
-
"content": m.content,
|
|
553
|
-
"timestamp": m.created_at.isoformat()
|
|
554
|
-
}
|
|
555
|
-
for m in messages
|
|
556
|
-
]
|
|
557
|
-
|
|
558
|
-
except Exception as e:
|
|
559
|
-
logger.error("Failed to get conversation messages", session_id=session_id, error=str(e))
|
|
560
|
-
return []
|
|
561
|
-
|
|
562
|
-
async def clear_conversation(self, session_id: str) -> int:
|
|
563
|
-
"""Clear all messages in a conversation session. Returns count deleted."""
|
|
564
|
-
try:
|
|
565
|
-
async with self.get_session() as session:
|
|
566
|
-
stmt = select(ConversationMessage).where(
|
|
567
|
-
ConversationMessage.session_id == session_id
|
|
568
|
-
)
|
|
569
|
-
result = await session.execute(stmt)
|
|
570
|
-
messages = result.scalars().all()
|
|
571
|
-
|
|
572
|
-
count = len(messages)
|
|
573
|
-
for message in messages:
|
|
574
|
-
await session.delete(message)
|
|
575
|
-
|
|
576
|
-
await session.commit()
|
|
577
|
-
logger.info(f"[Memory] Cleared {count} messages from session '{session_id}'")
|
|
578
|
-
return count
|
|
579
|
-
|
|
580
|
-
except Exception as e:
|
|
581
|
-
logger.error("Failed to clear conversation", session_id=session_id, error=str(e))
|
|
582
|
-
return 0
|
|
583
|
-
|
|
584
|
-
async def get_all_conversation_sessions(self) -> List[Dict[str, Any]]:
|
|
585
|
-
"""Get info about all conversation sessions."""
|
|
586
|
-
try:
|
|
587
|
-
async with self.get_session() as session:
|
|
588
|
-
# Get distinct session IDs with message count
|
|
589
|
-
from sqlalchemy import func as sql_func
|
|
590
|
-
stmt = select(
|
|
591
|
-
ConversationMessage.session_id,
|
|
592
|
-
sql_func.count(ConversationMessage.id).label('message_count'),
|
|
593
|
-
sql_func.min(ConversationMessage.created_at).label('created_at')
|
|
594
|
-
).group_by(ConversationMessage.session_id)
|
|
595
|
-
|
|
596
|
-
result = await session.execute(stmt)
|
|
597
|
-
rows = result.all()
|
|
598
|
-
|
|
599
|
-
return [
|
|
600
|
-
{
|
|
601
|
-
"session_id": row.session_id,
|
|
602
|
-
"message_count": row.message_count,
|
|
603
|
-
"created_at": row.created_at.isoformat() if row.created_at else None
|
|
604
|
-
}
|
|
605
|
-
for row in rows
|
|
606
|
-
]
|
|
607
|
-
|
|
608
|
-
except Exception as e:
|
|
609
|
-
logger.error("Failed to get conversation sessions", error=str(e))
|
|
610
|
-
return []
|
|
611
|
-
|
|
612
|
-
# ============================================================================
|
|
613
|
-
# Chat Messages (Console Panel persistence)
|
|
614
|
-
# ============================================================================
|
|
615
|
-
|
|
616
|
-
async def add_chat_message(self, session_id: str, role: str, message: str) -> bool:
|
|
617
|
-
"""Add a chat message to the console panel history."""
|
|
618
|
-
try:
|
|
619
|
-
async with self.get_session() as session:
|
|
620
|
-
chat_msg = ChatMessage(
|
|
621
|
-
session_id=session_id,
|
|
622
|
-
role=role,
|
|
623
|
-
message=message
|
|
624
|
-
)
|
|
625
|
-
session.add(chat_msg)
|
|
626
|
-
await session.commit()
|
|
627
|
-
logger.debug(f"[Chat] Added {role} message to session '{session_id}'")
|
|
628
|
-
return True
|
|
629
|
-
|
|
630
|
-
except Exception as e:
|
|
631
|
-
logger.error("Failed to add chat message", session_id=session_id, error=str(e))
|
|
632
|
-
return False
|
|
633
|
-
|
|
634
|
-
async def get_chat_messages(self, session_id: str, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
635
|
-
"""Get chat messages for a session, optionally limited to last N."""
|
|
636
|
-
try:
|
|
637
|
-
async with self.get_session() as session:
|
|
638
|
-
stmt = select(ChatMessage).where(
|
|
639
|
-
ChatMessage.session_id == session_id
|
|
640
|
-
).order_by(ChatMessage.created_at.asc())
|
|
641
|
-
|
|
642
|
-
result = await session.execute(stmt)
|
|
643
|
-
messages = result.scalars().all()
|
|
644
|
-
|
|
645
|
-
# Apply limit if specified
|
|
646
|
-
if limit and limit > 0:
|
|
647
|
-
messages = messages[-limit:]
|
|
648
|
-
|
|
649
|
-
return [
|
|
650
|
-
{
|
|
651
|
-
"role": m.role,
|
|
652
|
-
"message": m.message,
|
|
653
|
-
"timestamp": m.created_at.isoformat()
|
|
654
|
-
}
|
|
655
|
-
for m in messages
|
|
656
|
-
]
|
|
657
|
-
|
|
658
|
-
except Exception as e:
|
|
659
|
-
logger.error("Failed to get chat messages", session_id=session_id, error=str(e))
|
|
660
|
-
return []
|
|
661
|
-
|
|
662
|
-
async def clear_chat_messages(self, session_id: str) -> int:
|
|
663
|
-
"""Clear all chat messages for a session. Returns count deleted."""
|
|
664
|
-
try:
|
|
665
|
-
async with self.get_session() as session:
|
|
666
|
-
stmt = select(ChatMessage).where(
|
|
667
|
-
ChatMessage.session_id == session_id
|
|
668
|
-
)
|
|
669
|
-
result = await session.execute(stmt)
|
|
670
|
-
messages = result.scalars().all()
|
|
671
|
-
|
|
672
|
-
count = len(messages)
|
|
673
|
-
for message in messages:
|
|
674
|
-
await session.delete(message)
|
|
675
|
-
|
|
676
|
-
await session.commit()
|
|
677
|
-
logger.info(f"[Chat] Cleared {count} messages from session '{session_id}'")
|
|
678
|
-
return count
|
|
679
|
-
|
|
680
|
-
except Exception as e:
|
|
681
|
-
logger.error("Failed to clear chat messages", session_id=session_id, error=str(e))
|
|
682
|
-
return 0
|
|
683
|
-
|
|
684
|
-
async def get_chat_sessions(self) -> List[Dict[str, Any]]:
|
|
685
|
-
"""Get list of all chat sessions with message counts."""
|
|
686
|
-
try:
|
|
687
|
-
async with self.get_session() as session:
|
|
688
|
-
from sqlalchemy import func as sa_func
|
|
689
|
-
stmt = select(
|
|
690
|
-
ChatMessage.session_id,
|
|
691
|
-
sa_func.count(ChatMessage.id).label('message_count'),
|
|
692
|
-
sa_func.max(ChatMessage.created_at).label('last_message_at')
|
|
693
|
-
).group_by(ChatMessage.session_id).order_by(sa_func.max(ChatMessage.created_at).desc())
|
|
694
|
-
|
|
695
|
-
result = await session.execute(stmt)
|
|
696
|
-
rows = result.all()
|
|
697
|
-
|
|
698
|
-
return [
|
|
699
|
-
{
|
|
700
|
-
"session_id": row.session_id,
|
|
701
|
-
"message_count": row.message_count,
|
|
702
|
-
"last_message_at": row.last_message_at.isoformat() if row.last_message_at else None
|
|
703
|
-
}
|
|
704
|
-
for row in rows
|
|
705
|
-
]
|
|
706
|
-
|
|
707
|
-
except Exception as e:
|
|
708
|
-
logger.error("Failed to get chat sessions", error=str(e))
|
|
709
|
-
return []
|
|
710
|
-
|
|
711
|
-
# ============================================================================
|
|
712
|
-
#
|
|
713
|
-
# ============================================================================
|
|
714
|
-
|
|
715
|
-
async def
|
|
716
|
-
"""
|
|
717
|
-
import
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
)
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
try:
|
|
903
|
-
async with self.get_session() as session:
|
|
904
|
-
stmt = select(
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
try:
|
|
928
|
-
async with self.get_session() as session:
|
|
929
|
-
stmt = select(
|
|
930
|
-
result = await session.execute(stmt)
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
if
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
try:
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
await session.
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
return self._skill_to_dict(skill)
|
|
1169
|
-
|
|
1170
|
-
except Exception as e:
|
|
1171
|
-
logger.error("Failed to
|
|
1172
|
-
return None
|
|
1173
|
-
|
|
1174
|
-
async def
|
|
1175
|
-
"""
|
|
1176
|
-
try:
|
|
1177
|
-
async with self.get_session() as session:
|
|
1178
|
-
stmt = select(UserSkill).where(UserSkill.
|
|
1179
|
-
result = await session.execute(stmt)
|
|
1180
|
-
skill = result.scalar_one_or_none()
|
|
1181
|
-
|
|
1182
|
-
if skill
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
"
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1
|
+
"""Modern async database service with SQLModel and SQLAlchemy 2.0."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from typing import Dict, Any, List, Optional
|
|
5
|
+
from sqlmodel import SQLModel, select, Session
|
|
6
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
|
7
|
+
from sqlalchemy.exc import IntegrityError
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
|
|
10
|
+
from core.config import Settings
|
|
11
|
+
from models.database import NodeParameter, Workflow, Execution, APIKey, APIKeyValidation, NodeOutput, ConversationMessage, ToolSchema, UserSkill, ChatMessage
|
|
12
|
+
from models.cache import CacheEntry # SQLite-backed cache for Redis alternative
|
|
13
|
+
from models.auth import User # Import User model to ensure table creation
|
|
14
|
+
from core.logging import get_logger
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Database:
|
|
20
|
+
"""Async database service with SQLModel."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, settings: Settings):
|
|
23
|
+
self.settings = settings
|
|
24
|
+
self.engine = None
|
|
25
|
+
self.async_session = None
|
|
26
|
+
|
|
27
|
+
async def startup(self):
|
|
28
|
+
"""Initialize database connection and create tables."""
|
|
29
|
+
try:
|
|
30
|
+
# Disable verbose database and asyncio logging
|
|
31
|
+
import logging
|
|
32
|
+
logging.getLogger("aiosqlite").setLevel(logging.WARNING)
|
|
33
|
+
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
|
34
|
+
logging.getLogger("sqlalchemy.dialects").setLevel(logging.WARNING)
|
|
35
|
+
logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING)
|
|
36
|
+
|
|
37
|
+
# Create async engine
|
|
38
|
+
self.engine = create_async_engine(
|
|
39
|
+
self.settings.database_url,
|
|
40
|
+
echo=self.settings.database_echo,
|
|
41
|
+
pool_size=self.settings.database_pool_size,
|
|
42
|
+
max_overflow=self.settings.database_max_overflow,
|
|
43
|
+
future=True
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Create session factory
|
|
47
|
+
self.async_session = async_sessionmaker(
|
|
48
|
+
bind=self.engine,
|
|
49
|
+
class_=AsyncSession,
|
|
50
|
+
expire_on_commit=False
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Create tables
|
|
54
|
+
async with self.engine.begin() as conn:
|
|
55
|
+
await conn.run_sync(SQLModel.metadata.create_all)
|
|
56
|
+
|
|
57
|
+
logger.info("Database initialized successfully")
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error("Database startup failed", error=str(e))
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
async def shutdown(self):
|
|
64
|
+
"""Close database connections."""
|
|
65
|
+
if self.engine:
|
|
66
|
+
await self.engine.dispose()
|
|
67
|
+
logger.info("Database connections closed")
|
|
68
|
+
|
|
69
|
+
@asynccontextmanager
|
|
70
|
+
async def get_session(self):
|
|
71
|
+
"""Get async database session."""
|
|
72
|
+
if not self.async_session:
|
|
73
|
+
raise RuntimeError("Database not initialized")
|
|
74
|
+
|
|
75
|
+
async with self.async_session() as session:
|
|
76
|
+
try:
|
|
77
|
+
yield session
|
|
78
|
+
except Exception:
|
|
79
|
+
await session.rollback()
|
|
80
|
+
raise
|
|
81
|
+
finally:
|
|
82
|
+
await session.close()
|
|
83
|
+
|
|
84
|
+
# ============================================================================
|
|
85
|
+
# Node Parameters
|
|
86
|
+
# ============================================================================
|
|
87
|
+
|
|
88
|
+
async def save_node_parameters(self, node_id: str, parameters: Dict[str, Any]) -> bool:
|
|
89
|
+
"""Save or update node parameters."""
|
|
90
|
+
try:
|
|
91
|
+
async with self.get_session() as session:
|
|
92
|
+
# Try to get existing parameter
|
|
93
|
+
stmt = select(NodeParameter).where(NodeParameter.node_id == node_id)
|
|
94
|
+
result = await session.execute(stmt)
|
|
95
|
+
existing = result.scalar_one_or_none()
|
|
96
|
+
|
|
97
|
+
if existing:
|
|
98
|
+
existing.parameters = parameters
|
|
99
|
+
else:
|
|
100
|
+
existing = NodeParameter(
|
|
101
|
+
node_id=node_id,
|
|
102
|
+
parameters=parameters
|
|
103
|
+
)
|
|
104
|
+
session.add(existing)
|
|
105
|
+
|
|
106
|
+
await session.commit()
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error("Failed to save node parameters", node_id=node_id, error=str(e))
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
async def get_node_parameters(self, node_id: str) -> Optional[Dict[str, Any]]:
|
|
114
|
+
"""Get node parameters."""
|
|
115
|
+
try:
|
|
116
|
+
async with self.get_session() as session:
|
|
117
|
+
stmt = select(NodeParameter).where(NodeParameter.node_id == node_id)
|
|
118
|
+
result = await session.execute(stmt)
|
|
119
|
+
parameter = result.scalar_one_or_none()
|
|
120
|
+
|
|
121
|
+
return parameter.parameters if parameter else None
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error("Failed to get node parameters", node_id=node_id, error=str(e))
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
async def delete_node_parameters(self, node_id: str) -> bool:
|
|
128
|
+
"""Delete node parameters."""
|
|
129
|
+
try:
|
|
130
|
+
async with self.get_session() as session:
|
|
131
|
+
stmt = select(NodeParameter).where(NodeParameter.node_id == node_id)
|
|
132
|
+
result = await session.execute(stmt)
|
|
133
|
+
parameter = result.scalar_one_or_none()
|
|
134
|
+
|
|
135
|
+
if parameter:
|
|
136
|
+
await session.delete(parameter)
|
|
137
|
+
await session.commit()
|
|
138
|
+
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error("Failed to delete node parameters", node_id=node_id, error=str(e))
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
# ============================================================================
|
|
146
|
+
# Workflows
|
|
147
|
+
# ============================================================================
|
|
148
|
+
|
|
149
|
+
async def save_workflow(self, workflow_id: str, name: str, data: Dict[str, Any],
|
|
150
|
+
description: Optional[str] = None) -> bool:
|
|
151
|
+
"""Save or update workflow."""
|
|
152
|
+
try:
|
|
153
|
+
async with self.get_session() as session:
|
|
154
|
+
stmt = select(Workflow).where(Workflow.id == workflow_id)
|
|
155
|
+
result = await session.execute(stmt)
|
|
156
|
+
existing = result.scalar_one_or_none()
|
|
157
|
+
|
|
158
|
+
if existing:
|
|
159
|
+
existing.name = name
|
|
160
|
+
existing.description = description
|
|
161
|
+
existing.data = data
|
|
162
|
+
else:
|
|
163
|
+
existing = Workflow(
|
|
164
|
+
id=workflow_id,
|
|
165
|
+
name=name,
|
|
166
|
+
description=description,
|
|
167
|
+
data=data
|
|
168
|
+
)
|
|
169
|
+
session.add(existing)
|
|
170
|
+
|
|
171
|
+
await session.commit()
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error("Failed to save workflow", workflow_id=workflow_id, error=str(e))
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
async def get_workflow(self, workflow_id: str) -> Optional[Workflow]:
|
|
179
|
+
"""Get workflow by ID."""
|
|
180
|
+
try:
|
|
181
|
+
async with self.get_session() as session:
|
|
182
|
+
stmt = select(Workflow).where(Workflow.id == workflow_id)
|
|
183
|
+
result = await session.execute(stmt)
|
|
184
|
+
return result.scalar_one_or_none()
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.error("Failed to get workflow", workflow_id=workflow_id, error=str(e))
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
async def get_all_workflows(self) -> List[Workflow]:
|
|
191
|
+
"""Get all workflows."""
|
|
192
|
+
try:
|
|
193
|
+
async with self.get_session() as session:
|
|
194
|
+
stmt = select(Workflow).order_by(Workflow.updated_at.desc())
|
|
195
|
+
result = await session.execute(stmt)
|
|
196
|
+
return result.scalars().all()
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error("Failed to get all workflows", error=str(e))
|
|
200
|
+
return []
|
|
201
|
+
|
|
202
|
+
async def delete_workflow(self, workflow_id: str) -> bool:
|
|
203
|
+
"""Delete workflow."""
|
|
204
|
+
try:
|
|
205
|
+
async with self.get_session() as session:
|
|
206
|
+
stmt = select(Workflow).where(Workflow.id == workflow_id)
|
|
207
|
+
result = await session.execute(stmt)
|
|
208
|
+
workflow = result.scalar_one_or_none()
|
|
209
|
+
|
|
210
|
+
if workflow:
|
|
211
|
+
await session.delete(workflow)
|
|
212
|
+
await session.commit()
|
|
213
|
+
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error("Failed to delete workflow", workflow_id=workflow_id, error=str(e))
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
# ============================================================================
|
|
221
|
+
# Executions
|
|
222
|
+
# ============================================================================
|
|
223
|
+
|
|
224
|
+
async def save_execution(self, execution_id: str, workflow_id: str, node_id: str,
|
|
225
|
+
status: str, result: Optional[Dict[str, Any]] = None,
|
|
226
|
+
error: Optional[str] = None, execution_time: Optional[float] = None) -> bool:
|
|
227
|
+
"""Save execution result."""
|
|
228
|
+
try:
|
|
229
|
+
async with self.get_session() as session:
|
|
230
|
+
execution = Execution(
|
|
231
|
+
id=execution_id,
|
|
232
|
+
workflow_id=workflow_id,
|
|
233
|
+
node_id=node_id,
|
|
234
|
+
status=status,
|
|
235
|
+
result=result,
|
|
236
|
+
error=error,
|
|
237
|
+
execution_time=execution_time
|
|
238
|
+
)
|
|
239
|
+
session.add(execution)
|
|
240
|
+
await session.commit()
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error("Failed to save execution", execution_id=execution_id, error=str(e))
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
async def get_execution(self, execution_id: str) -> Optional[Execution]:
|
|
248
|
+
"""Get execution by ID."""
|
|
249
|
+
try:
|
|
250
|
+
async with self.get_session() as session:
|
|
251
|
+
stmt = select(Execution).where(Execution.id == execution_id)
|
|
252
|
+
result = await session.execute(stmt)
|
|
253
|
+
return result.scalar_one_or_none()
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.error("Failed to get execution", execution_id=execution_id, error=str(e))
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
# ============================================================================
|
|
260
|
+
# API Keys
|
|
261
|
+
# ============================================================================
|
|
262
|
+
|
|
263
|
+
async def save_api_key(self, key_id: str, provider: str, session_id: str,
|
|
264
|
+
key_encrypted: str, key_hash: str,
|
|
265
|
+
models: Optional[List[str]] = None) -> bool:
|
|
266
|
+
"""Save encrypted API key."""
|
|
267
|
+
logger.info(f"Database save_api_key called with key_id: {key_id}, provider: {provider}")
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
async with self.get_session() as session:
|
|
271
|
+
api_key = APIKey(
|
|
272
|
+
id=key_id,
|
|
273
|
+
provider=provider,
|
|
274
|
+
session_id=session_id,
|
|
275
|
+
key_encrypted=key_encrypted,
|
|
276
|
+
key_hash=key_hash,
|
|
277
|
+
models={"models": models} if models else None,
|
|
278
|
+
last_validated=datetime.now(timezone.utc)
|
|
279
|
+
)
|
|
280
|
+
session.add(api_key)
|
|
281
|
+
await session.commit()
|
|
282
|
+
logger.info(f"Successfully saved new API key: {key_id}")
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
except IntegrityError as e:
|
|
286
|
+
logger.info(f"API key {key_id} already exists, attempting update. Error: {str(e)}")
|
|
287
|
+
# Key already exists, update it
|
|
288
|
+
try:
|
|
289
|
+
async with self.get_session() as session:
|
|
290
|
+
stmt = select(APIKey).where(APIKey.id == key_id)
|
|
291
|
+
result = await session.execute(stmt)
|
|
292
|
+
existing = result.scalar_one_or_none()
|
|
293
|
+
|
|
294
|
+
if existing:
|
|
295
|
+
logger.info(f"Found existing API key {key_id}, updating...")
|
|
296
|
+
existing.key_encrypted = key_encrypted
|
|
297
|
+
existing.key_hash = key_hash
|
|
298
|
+
existing.models = {"models": models} if models else None
|
|
299
|
+
existing.last_validated = datetime.now(timezone.utc)
|
|
300
|
+
await session.commit()
|
|
301
|
+
logger.info(f"Successfully updated API key: {key_id}")
|
|
302
|
+
return True
|
|
303
|
+
else:
|
|
304
|
+
logger.error(f"Could not find existing API key {key_id} for update")
|
|
305
|
+
return False
|
|
306
|
+
except Exception as update_e:
|
|
307
|
+
logger.error(f"Failed to update API key {key_id}", error=str(update_e))
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
except Exception as e:
|
|
311
|
+
logger.error("Failed to save API key", provider=provider, error=str(e))
|
|
312
|
+
import traceback
|
|
313
|
+
logger.error("Full traceback", traceback=traceback.format_exc())
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
async def get_api_key(self, key_id: str) -> Optional[APIKey]:
|
|
317
|
+
"""Get API key by ID."""
|
|
318
|
+
try:
|
|
319
|
+
async with self.get_session() as session:
|
|
320
|
+
stmt = select(APIKey).where(APIKey.id == key_id)
|
|
321
|
+
result = await session.execute(stmt)
|
|
322
|
+
return result.scalar_one_or_none()
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.error("Failed to get API key", key_id=key_id, error=str(e))
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
async def get_api_key_by_provider(self, provider: str, session_id: str = "default") -> Optional[APIKey]:
|
|
329
|
+
"""Get API key by provider and session."""
|
|
330
|
+
try:
|
|
331
|
+
async with self.get_session() as session:
|
|
332
|
+
stmt = select(APIKey).where(
|
|
333
|
+
APIKey.provider == provider,
|
|
334
|
+
APIKey.session_id == session_id,
|
|
335
|
+
APIKey.is_valid == True
|
|
336
|
+
)
|
|
337
|
+
result = await session.execute(stmt)
|
|
338
|
+
return result.scalar_one_or_none()
|
|
339
|
+
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.error("Failed to get API key by provider", provider=provider, error=str(e))
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
async def delete_api_key(self, provider: str, session_id: str = "default") -> bool:
|
|
345
|
+
"""Delete API key."""
|
|
346
|
+
try:
|
|
347
|
+
async with self.get_session() as session:
|
|
348
|
+
stmt = select(APIKey).where(
|
|
349
|
+
APIKey.provider == provider,
|
|
350
|
+
APIKey.session_id == session_id
|
|
351
|
+
)
|
|
352
|
+
result = await session.execute(stmt)
|
|
353
|
+
api_key = result.scalar_one_or_none()
|
|
354
|
+
|
|
355
|
+
if api_key:
|
|
356
|
+
await session.delete(api_key)
|
|
357
|
+
await session.commit()
|
|
358
|
+
logger.debug("API key deleted", provider=provider, session_id=session_id)
|
|
359
|
+
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.error("Failed to delete API key", provider=provider, error=str(e))
|
|
364
|
+
return False
|
|
365
|
+
|
|
366
|
+
# ============================================================================
|
|
367
|
+
# API Key Validation Cache
|
|
368
|
+
# ============================================================================
|
|
369
|
+
|
|
370
|
+
async def save_api_key_validation(self, key_hash: str) -> bool:
|
|
371
|
+
"""Save API key validation status."""
|
|
372
|
+
try:
|
|
373
|
+
async with self.get_session() as session:
|
|
374
|
+
validation = APIKeyValidation(
|
|
375
|
+
key_hash=key_hash,
|
|
376
|
+
validated=True
|
|
377
|
+
)
|
|
378
|
+
session.add(validation)
|
|
379
|
+
await session.commit()
|
|
380
|
+
return True
|
|
381
|
+
|
|
382
|
+
except IntegrityError:
|
|
383
|
+
# Already exists, update timestamp
|
|
384
|
+
async with self.get_session() as session:
|
|
385
|
+
stmt = select(APIKeyValidation).where(APIKeyValidation.key_hash == key_hash)
|
|
386
|
+
result = await session.execute(stmt)
|
|
387
|
+
existing = result.scalar_one_or_none()
|
|
388
|
+
|
|
389
|
+
if existing:
|
|
390
|
+
existing.timestamp = datetime.now(timezone.utc)
|
|
391
|
+
await session.commit()
|
|
392
|
+
return True
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
logger.error("Failed to save API key validation", key_hash=key_hash, error=str(e))
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
async def is_api_key_validated(self, key_hash: str) -> bool:
|
|
400
|
+
"""Check if API key is validated."""
|
|
401
|
+
try:
|
|
402
|
+
async with self.get_session() as session:
|
|
403
|
+
stmt = select(APIKeyValidation).where(APIKeyValidation.key_hash == key_hash)
|
|
404
|
+
result = await session.execute(stmt)
|
|
405
|
+
validation = result.scalar_one_or_none()
|
|
406
|
+
return validation is not None and validation.validated
|
|
407
|
+
|
|
408
|
+
except Exception as e:
|
|
409
|
+
logger.error("Failed to check API key validation", key_hash=key_hash, error=str(e))
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
# ============================================================================
|
|
413
|
+
# Node Outputs
|
|
414
|
+
# ============================================================================
|
|
415
|
+
|
|
416
|
+
async def save_node_output(self, node_id: str, session_id: str, output_name: str,
|
|
417
|
+
data: Dict[str, Any]) -> bool:
|
|
418
|
+
"""Save or update node output."""
|
|
419
|
+
try:
|
|
420
|
+
async with self.get_session() as session:
|
|
421
|
+
# Try to get existing output
|
|
422
|
+
stmt = select(NodeOutput).where(
|
|
423
|
+
NodeOutput.node_id == node_id,
|
|
424
|
+
NodeOutput.session_id == session_id,
|
|
425
|
+
NodeOutput.output_name == output_name
|
|
426
|
+
)
|
|
427
|
+
result = await session.execute(stmt)
|
|
428
|
+
existing = result.scalar_one_or_none()
|
|
429
|
+
|
|
430
|
+
action = "updated"
|
|
431
|
+
if existing:
|
|
432
|
+
existing.data = data
|
|
433
|
+
else:
|
|
434
|
+
action = "inserted"
|
|
435
|
+
existing = NodeOutput(
|
|
436
|
+
node_id=node_id,
|
|
437
|
+
session_id=session_id,
|
|
438
|
+
output_name=output_name,
|
|
439
|
+
data=data
|
|
440
|
+
)
|
|
441
|
+
session.add(existing)
|
|
442
|
+
|
|
443
|
+
await session.commit()
|
|
444
|
+
logger.info("[DB] Node output saved", action=action, node_id=node_id, session_id=session_id, output_name=output_name)
|
|
445
|
+
return True
|
|
446
|
+
|
|
447
|
+
except Exception as e:
|
|
448
|
+
logger.error("Failed to save node output", node_id=node_id, error=str(e))
|
|
449
|
+
import traceback
|
|
450
|
+
traceback.print_exc()
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
async def get_node_output(self, node_id: str, session_id: str = "default",
|
|
454
|
+
output_name: str = "output_0") -> Optional[Dict[str, Any]]:
|
|
455
|
+
"""Get node output data."""
|
|
456
|
+
try:
|
|
457
|
+
async with self.get_session() as session:
|
|
458
|
+
stmt = select(NodeOutput).where(
|
|
459
|
+
NodeOutput.node_id == node_id,
|
|
460
|
+
NodeOutput.session_id == session_id,
|
|
461
|
+
NodeOutput.output_name == output_name
|
|
462
|
+
)
|
|
463
|
+
result = await session.execute(stmt)
|
|
464
|
+
output = result.scalar_one_or_none()
|
|
465
|
+
|
|
466
|
+
return output.data if output else None
|
|
467
|
+
|
|
468
|
+
except Exception as e:
|
|
469
|
+
logger.error("Failed to get node output", node_id=node_id, error=str(e))
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
async def delete_node_output(self, node_id: str) -> int:
|
|
473
|
+
"""Delete all outputs for a node (any session). Returns count deleted."""
|
|
474
|
+
try:
|
|
475
|
+
async with self.get_session() as session:
|
|
476
|
+
stmt = select(NodeOutput).where(NodeOutput.node_id == node_id)
|
|
477
|
+
result = await session.execute(stmt)
|
|
478
|
+
outputs = result.scalars().all()
|
|
479
|
+
|
|
480
|
+
count = len(outputs)
|
|
481
|
+
for output in outputs:
|
|
482
|
+
await session.delete(output)
|
|
483
|
+
|
|
484
|
+
await session.commit()
|
|
485
|
+
logger.info("Deleted node outputs", node_id=node_id, count=count)
|
|
486
|
+
return count
|
|
487
|
+
|
|
488
|
+
except Exception as e:
|
|
489
|
+
logger.error("Failed to delete node output", node_id=node_id, error=str(e))
|
|
490
|
+
return 0
|
|
491
|
+
|
|
492
|
+
async def clear_session_outputs(self, session_id: str = "default") -> int:
|
|
493
|
+
"""Clear all outputs for a session. Returns count deleted."""
|
|
494
|
+
try:
|
|
495
|
+
async with self.get_session() as session:
|
|
496
|
+
stmt = select(NodeOutput).where(NodeOutput.session_id == session_id)
|
|
497
|
+
result = await session.execute(stmt)
|
|
498
|
+
outputs = result.scalars().all()
|
|
499
|
+
|
|
500
|
+
count = len(outputs)
|
|
501
|
+
for output in outputs:
|
|
502
|
+
await session.delete(output)
|
|
503
|
+
|
|
504
|
+
await session.commit()
|
|
505
|
+
logger.info("Cleared session outputs", session_id=session_id, count=count)
|
|
506
|
+
return count
|
|
507
|
+
|
|
508
|
+
except Exception as e:
|
|
509
|
+
logger.error("Failed to clear session outputs", session_id=session_id, error=str(e))
|
|
510
|
+
return 0
|
|
511
|
+
|
|
512
|
+
# ============================================================================
|
|
513
|
+
# Conversation Messages (AI Memory)
|
|
514
|
+
# ============================================================================
|
|
515
|
+
|
|
516
|
+
async def add_conversation_message(self, session_id: str, role: str, content: str) -> bool:
|
|
517
|
+
"""Add a message to conversation history."""
|
|
518
|
+
try:
|
|
519
|
+
async with self.get_session() as session:
|
|
520
|
+
message = ConversationMessage(
|
|
521
|
+
session_id=session_id,
|
|
522
|
+
role=role,
|
|
523
|
+
content=content
|
|
524
|
+
)
|
|
525
|
+
session.add(message)
|
|
526
|
+
await session.commit()
|
|
527
|
+
logger.info(f"[Memory] Added {role} message to session '{session_id}'")
|
|
528
|
+
return True
|
|
529
|
+
|
|
530
|
+
except Exception as e:
|
|
531
|
+
logger.error("Failed to add conversation message", session_id=session_id, error=str(e))
|
|
532
|
+
return False
|
|
533
|
+
|
|
534
|
+
async def get_conversation_messages(self, session_id: str, window_size: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
535
|
+
"""Get conversation messages, optionally limited to last N."""
|
|
536
|
+
try:
|
|
537
|
+
async with self.get_session() as session:
|
|
538
|
+
stmt = select(ConversationMessage).where(
|
|
539
|
+
ConversationMessage.session_id == session_id
|
|
540
|
+
).order_by(ConversationMessage.created_at.asc())
|
|
541
|
+
|
|
542
|
+
result = await session.execute(stmt)
|
|
543
|
+
messages = result.scalars().all()
|
|
544
|
+
|
|
545
|
+
# Apply window limit if specified
|
|
546
|
+
if window_size and window_size > 0:
|
|
547
|
+
messages = messages[-window_size:]
|
|
548
|
+
|
|
549
|
+
return [
|
|
550
|
+
{
|
|
551
|
+
"role": m.role,
|
|
552
|
+
"content": m.content,
|
|
553
|
+
"timestamp": m.created_at.isoformat()
|
|
554
|
+
}
|
|
555
|
+
for m in messages
|
|
556
|
+
]
|
|
557
|
+
|
|
558
|
+
except Exception as e:
|
|
559
|
+
logger.error("Failed to get conversation messages", session_id=session_id, error=str(e))
|
|
560
|
+
return []
|
|
561
|
+
|
|
562
|
+
async def clear_conversation(self, session_id: str) -> int:
|
|
563
|
+
"""Clear all messages in a conversation session. Returns count deleted."""
|
|
564
|
+
try:
|
|
565
|
+
async with self.get_session() as session:
|
|
566
|
+
stmt = select(ConversationMessage).where(
|
|
567
|
+
ConversationMessage.session_id == session_id
|
|
568
|
+
)
|
|
569
|
+
result = await session.execute(stmt)
|
|
570
|
+
messages = result.scalars().all()
|
|
571
|
+
|
|
572
|
+
count = len(messages)
|
|
573
|
+
for message in messages:
|
|
574
|
+
await session.delete(message)
|
|
575
|
+
|
|
576
|
+
await session.commit()
|
|
577
|
+
logger.info(f"[Memory] Cleared {count} messages from session '{session_id}'")
|
|
578
|
+
return count
|
|
579
|
+
|
|
580
|
+
except Exception as e:
|
|
581
|
+
logger.error("Failed to clear conversation", session_id=session_id, error=str(e))
|
|
582
|
+
return 0
|
|
583
|
+
|
|
584
|
+
async def get_all_conversation_sessions(self) -> List[Dict[str, Any]]:
|
|
585
|
+
"""Get info about all conversation sessions."""
|
|
586
|
+
try:
|
|
587
|
+
async with self.get_session() as session:
|
|
588
|
+
# Get distinct session IDs with message count
|
|
589
|
+
from sqlalchemy import func as sql_func
|
|
590
|
+
stmt = select(
|
|
591
|
+
ConversationMessage.session_id,
|
|
592
|
+
sql_func.count(ConversationMessage.id).label('message_count'),
|
|
593
|
+
sql_func.min(ConversationMessage.created_at).label('created_at')
|
|
594
|
+
).group_by(ConversationMessage.session_id)
|
|
595
|
+
|
|
596
|
+
result = await session.execute(stmt)
|
|
597
|
+
rows = result.all()
|
|
598
|
+
|
|
599
|
+
return [
|
|
600
|
+
{
|
|
601
|
+
"session_id": row.session_id,
|
|
602
|
+
"message_count": row.message_count,
|
|
603
|
+
"created_at": row.created_at.isoformat() if row.created_at else None
|
|
604
|
+
}
|
|
605
|
+
for row in rows
|
|
606
|
+
]
|
|
607
|
+
|
|
608
|
+
except Exception as e:
|
|
609
|
+
logger.error("Failed to get conversation sessions", error=str(e))
|
|
610
|
+
return []
|
|
611
|
+
|
|
612
|
+
# ============================================================================
|
|
613
|
+
# Chat Messages (Console Panel persistence)
|
|
614
|
+
# ============================================================================
|
|
615
|
+
|
|
616
|
+
async def add_chat_message(self, session_id: str, role: str, message: str) -> bool:
|
|
617
|
+
"""Add a chat message to the console panel history."""
|
|
618
|
+
try:
|
|
619
|
+
async with self.get_session() as session:
|
|
620
|
+
chat_msg = ChatMessage(
|
|
621
|
+
session_id=session_id,
|
|
622
|
+
role=role,
|
|
623
|
+
message=message
|
|
624
|
+
)
|
|
625
|
+
session.add(chat_msg)
|
|
626
|
+
await session.commit()
|
|
627
|
+
logger.debug(f"[Chat] Added {role} message to session '{session_id}'")
|
|
628
|
+
return True
|
|
629
|
+
|
|
630
|
+
except Exception as e:
|
|
631
|
+
logger.error("Failed to add chat message", session_id=session_id, error=str(e))
|
|
632
|
+
return False
|
|
633
|
+
|
|
634
|
+
async def get_chat_messages(self, session_id: str, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
635
|
+
"""Get chat messages for a session, optionally limited to last N."""
|
|
636
|
+
try:
|
|
637
|
+
async with self.get_session() as session:
|
|
638
|
+
stmt = select(ChatMessage).where(
|
|
639
|
+
ChatMessage.session_id == session_id
|
|
640
|
+
).order_by(ChatMessage.created_at.asc())
|
|
641
|
+
|
|
642
|
+
result = await session.execute(stmt)
|
|
643
|
+
messages = result.scalars().all()
|
|
644
|
+
|
|
645
|
+
# Apply limit if specified
|
|
646
|
+
if limit and limit > 0:
|
|
647
|
+
messages = messages[-limit:]
|
|
648
|
+
|
|
649
|
+
return [
|
|
650
|
+
{
|
|
651
|
+
"role": m.role,
|
|
652
|
+
"message": m.message,
|
|
653
|
+
"timestamp": m.created_at.isoformat()
|
|
654
|
+
}
|
|
655
|
+
for m in messages
|
|
656
|
+
]
|
|
657
|
+
|
|
658
|
+
except Exception as e:
|
|
659
|
+
logger.error("Failed to get chat messages", session_id=session_id, error=str(e))
|
|
660
|
+
return []
|
|
661
|
+
|
|
662
|
+
async def clear_chat_messages(self, session_id: str) -> int:
|
|
663
|
+
"""Clear all chat messages for a session. Returns count deleted."""
|
|
664
|
+
try:
|
|
665
|
+
async with self.get_session() as session:
|
|
666
|
+
stmt = select(ChatMessage).where(
|
|
667
|
+
ChatMessage.session_id == session_id
|
|
668
|
+
)
|
|
669
|
+
result = await session.execute(stmt)
|
|
670
|
+
messages = result.scalars().all()
|
|
671
|
+
|
|
672
|
+
count = len(messages)
|
|
673
|
+
for message in messages:
|
|
674
|
+
await session.delete(message)
|
|
675
|
+
|
|
676
|
+
await session.commit()
|
|
677
|
+
logger.info(f"[Chat] Cleared {count} messages from session '{session_id}'")
|
|
678
|
+
return count
|
|
679
|
+
|
|
680
|
+
except Exception as e:
|
|
681
|
+
logger.error("Failed to clear chat messages", session_id=session_id, error=str(e))
|
|
682
|
+
return 0
|
|
683
|
+
|
|
684
|
+
async def get_chat_sessions(self) -> List[Dict[str, Any]]:
|
|
685
|
+
"""Get list of all chat sessions with message counts."""
|
|
686
|
+
try:
|
|
687
|
+
async with self.get_session() as session:
|
|
688
|
+
from sqlalchemy import func as sa_func
|
|
689
|
+
stmt = select(
|
|
690
|
+
ChatMessage.session_id,
|
|
691
|
+
sa_func.count(ChatMessage.id).label('message_count'),
|
|
692
|
+
sa_func.max(ChatMessage.created_at).label('last_message_at')
|
|
693
|
+
).group_by(ChatMessage.session_id).order_by(sa_func.max(ChatMessage.created_at).desc())
|
|
694
|
+
|
|
695
|
+
result = await session.execute(stmt)
|
|
696
|
+
rows = result.all()
|
|
697
|
+
|
|
698
|
+
return [
|
|
699
|
+
{
|
|
700
|
+
"session_id": row.session_id,
|
|
701
|
+
"message_count": row.message_count,
|
|
702
|
+
"last_message_at": row.last_message_at.isoformat() if row.last_message_at else None
|
|
703
|
+
}
|
|
704
|
+
for row in rows
|
|
705
|
+
]
|
|
706
|
+
|
|
707
|
+
except Exception as e:
|
|
708
|
+
logger.error("Failed to get chat sessions", error=str(e))
|
|
709
|
+
return []
|
|
710
|
+
|
|
711
|
+
# ============================================================================
|
|
712
|
+
# Console Logs (Console Panel persistence)
|
|
713
|
+
# ============================================================================
|
|
714
|
+
|
|
715
|
+
async def add_console_log(self, log_data: Dict[str, Any]) -> bool:
|
|
716
|
+
"""Add a console log entry to the database."""
|
|
717
|
+
from models.database import ConsoleLog
|
|
718
|
+
import json
|
|
719
|
+
|
|
720
|
+
try:
|
|
721
|
+
async with self.get_session() as session:
|
|
722
|
+
console_log = ConsoleLog(
|
|
723
|
+
node_id=log_data.get("node_id", ""),
|
|
724
|
+
label=log_data.get("label", ""),
|
|
725
|
+
workflow_id=log_data.get("workflow_id"),
|
|
726
|
+
data=json.dumps(log_data.get("data", {})),
|
|
727
|
+
formatted=log_data.get("formatted", ""),
|
|
728
|
+
format=log_data.get("format", "text"),
|
|
729
|
+
source_node_id=log_data.get("source_node_id"),
|
|
730
|
+
source_node_type=log_data.get("source_node_type"),
|
|
731
|
+
source_node_label=log_data.get("source_node_label"),
|
|
732
|
+
)
|
|
733
|
+
session.add(console_log)
|
|
734
|
+
await session.commit()
|
|
735
|
+
logger.debug(f"[Console] Added log from node '{log_data.get('node_id')}'")
|
|
736
|
+
return True
|
|
737
|
+
|
|
738
|
+
except Exception as e:
|
|
739
|
+
logger.error("Failed to add console log", error=str(e))
|
|
740
|
+
return False
|
|
741
|
+
|
|
742
|
+
async def get_console_logs(self, limit: int = 100) -> List[Dict[str, Any]]:
|
|
743
|
+
"""Get console logs, optionally limited to last N entries."""
|
|
744
|
+
from models.database import ConsoleLog
|
|
745
|
+
import json
|
|
746
|
+
|
|
747
|
+
try:
|
|
748
|
+
async with self.get_session() as session:
|
|
749
|
+
stmt = select(ConsoleLog).order_by(ConsoleLog.created_at.desc()).limit(limit)
|
|
750
|
+
|
|
751
|
+
result = await session.execute(stmt)
|
|
752
|
+
logs = result.scalars().all()
|
|
753
|
+
|
|
754
|
+
# Return in chronological order (oldest first)
|
|
755
|
+
return [
|
|
756
|
+
{
|
|
757
|
+
"node_id": log.node_id,
|
|
758
|
+
"label": log.label,
|
|
759
|
+
"workflow_id": log.workflow_id,
|
|
760
|
+
"data": json.loads(log.data) if log.data else {},
|
|
761
|
+
"formatted": log.formatted,
|
|
762
|
+
"format": log.format,
|
|
763
|
+
"source_node_id": log.source_node_id,
|
|
764
|
+
"source_node_type": log.source_node_type,
|
|
765
|
+
"source_node_label": log.source_node_label,
|
|
766
|
+
"timestamp": log.created_at.isoformat(),
|
|
767
|
+
}
|
|
768
|
+
for log in reversed(logs)
|
|
769
|
+
]
|
|
770
|
+
|
|
771
|
+
except Exception as e:
|
|
772
|
+
logger.error("Failed to get console logs", error=str(e))
|
|
773
|
+
return []
|
|
774
|
+
|
|
775
|
+
async def clear_console_logs(self) -> int:
|
|
776
|
+
"""Clear all console logs. Returns count deleted."""
|
|
777
|
+
from models.database import ConsoleLog
|
|
778
|
+
|
|
779
|
+
try:
|
|
780
|
+
async with self.get_session() as session:
|
|
781
|
+
stmt = select(ConsoleLog)
|
|
782
|
+
result = await session.execute(stmt)
|
|
783
|
+
logs = result.scalars().all()
|
|
784
|
+
|
|
785
|
+
count = len(logs)
|
|
786
|
+
for log in logs:
|
|
787
|
+
await session.delete(log)
|
|
788
|
+
|
|
789
|
+
await session.commit()
|
|
790
|
+
logger.info(f"[Console] Cleared {count} console logs")
|
|
791
|
+
return count
|
|
792
|
+
|
|
793
|
+
except Exception as e:
|
|
794
|
+
logger.error("Failed to clear console logs", error=str(e))
|
|
795
|
+
return 0
|
|
796
|
+
|
|
797
|
+
# ============================================================================
|
|
798
|
+
# Cache Entries (SQLite-backed Redis alternative)
|
|
799
|
+
# ============================================================================
|
|
800
|
+
|
|
801
|
+
async def get_cache_entry(self, key: str) -> Optional[str]:
|
|
802
|
+
"""Get cache value by key. Returns None if expired or not found."""
|
|
803
|
+
import time
|
|
804
|
+
try:
|
|
805
|
+
async with self.get_session() as session:
|
|
806
|
+
stmt = select(CacheEntry).where(CacheEntry.key == key)
|
|
807
|
+
result = await session.execute(stmt)
|
|
808
|
+
entry = result.scalar_one_or_none()
|
|
809
|
+
|
|
810
|
+
if not entry:
|
|
811
|
+
return None
|
|
812
|
+
|
|
813
|
+
# Check expiration
|
|
814
|
+
if entry.expires_at and entry.expires_at < time.time():
|
|
815
|
+
# Entry expired - delete it
|
|
816
|
+
await session.delete(entry)
|
|
817
|
+
await session.commit()
|
|
818
|
+
return None
|
|
819
|
+
|
|
820
|
+
return entry.value
|
|
821
|
+
|
|
822
|
+
except Exception as e:
|
|
823
|
+
logger.error("Failed to get cache entry", key=key, error=str(e))
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
async def set_cache_entry(self, key: str, value: str, ttl: Optional[int] = None) -> bool:
|
|
827
|
+
"""Set cache value with optional TTL in seconds."""
|
|
828
|
+
import time
|
|
829
|
+
try:
|
|
830
|
+
expires_at = time.time() + ttl if ttl else None
|
|
831
|
+
|
|
832
|
+
async with self.get_session() as session:
|
|
833
|
+
# Try to get existing entry
|
|
834
|
+
stmt = select(CacheEntry).where(CacheEntry.key == key)
|
|
835
|
+
result = await session.execute(stmt)
|
|
836
|
+
existing = result.scalar_one_or_none()
|
|
837
|
+
|
|
838
|
+
if existing:
|
|
839
|
+
existing.value = value
|
|
840
|
+
existing.expires_at = expires_at
|
|
841
|
+
existing.created_at = time.time()
|
|
842
|
+
else:
|
|
843
|
+
entry = CacheEntry(
|
|
844
|
+
key=key,
|
|
845
|
+
value=value,
|
|
846
|
+
expires_at=expires_at,
|
|
847
|
+
created_at=time.time()
|
|
848
|
+
)
|
|
849
|
+
session.add(entry)
|
|
850
|
+
|
|
851
|
+
await session.commit()
|
|
852
|
+
return True
|
|
853
|
+
|
|
854
|
+
except Exception as e:
|
|
855
|
+
logger.error("Failed to set cache entry", key=key, error=str(e))
|
|
856
|
+
return False
|
|
857
|
+
|
|
858
|
+
async def delete_cache_entry(self, key: str) -> bool:
|
|
859
|
+
"""Delete cache entry by key."""
|
|
860
|
+
try:
|
|
861
|
+
async with self.get_session() as session:
|
|
862
|
+
stmt = select(CacheEntry).where(CacheEntry.key == key)
|
|
863
|
+
result = await session.execute(stmt)
|
|
864
|
+
entry = result.scalar_one_or_none()
|
|
865
|
+
|
|
866
|
+
if entry:
|
|
867
|
+
await session.delete(entry)
|
|
868
|
+
await session.commit()
|
|
869
|
+
|
|
870
|
+
return True
|
|
871
|
+
|
|
872
|
+
except Exception as e:
|
|
873
|
+
logger.error("Failed to delete cache entry", key=key, error=str(e))
|
|
874
|
+
return False
|
|
875
|
+
|
|
876
|
+
async def delete_cache_pattern(self, pattern: str) -> int:
|
|
877
|
+
"""Delete cache entries matching pattern (uses SQL LIKE)."""
|
|
878
|
+
try:
|
|
879
|
+
# Convert glob pattern to SQL LIKE pattern
|
|
880
|
+
sql_pattern = pattern.replace("*", "%")
|
|
881
|
+
|
|
882
|
+
async with self.get_session() as session:
|
|
883
|
+
stmt = select(CacheEntry).where(CacheEntry.key.like(sql_pattern))
|
|
884
|
+
result = await session.execute(stmt)
|
|
885
|
+
entries = result.scalars().all()
|
|
886
|
+
|
|
887
|
+
count = len(entries)
|
|
888
|
+
for entry in entries:
|
|
889
|
+
await session.delete(entry)
|
|
890
|
+
|
|
891
|
+
await session.commit()
|
|
892
|
+
logger.debug("Deleted cache entries", pattern=pattern, count=count)
|
|
893
|
+
return count
|
|
894
|
+
|
|
895
|
+
except Exception as e:
|
|
896
|
+
logger.error("Failed to delete cache pattern", pattern=pattern, error=str(e))
|
|
897
|
+
return 0
|
|
898
|
+
|
|
899
|
+
async def cleanup_expired_cache(self) -> int:
|
|
900
|
+
"""Remove all expired cache entries. Returns count deleted."""
|
|
901
|
+
import time
|
|
902
|
+
try:
|
|
903
|
+
async with self.get_session() as session:
|
|
904
|
+
stmt = select(CacheEntry).where(
|
|
905
|
+
CacheEntry.expires_at.isnot(None),
|
|
906
|
+
CacheEntry.expires_at < time.time()
|
|
907
|
+
)
|
|
908
|
+
result = await session.execute(stmt)
|
|
909
|
+
entries = result.scalars().all()
|
|
910
|
+
|
|
911
|
+
count = len(entries)
|
|
912
|
+
for entry in entries:
|
|
913
|
+
await session.delete(entry)
|
|
914
|
+
|
|
915
|
+
await session.commit()
|
|
916
|
+
if count > 0:
|
|
917
|
+
logger.info("Cleaned up expired cache entries", count=count)
|
|
918
|
+
return count
|
|
919
|
+
|
|
920
|
+
except Exception as e:
|
|
921
|
+
logger.error("Failed to cleanup expired cache", error=str(e))
|
|
922
|
+
return 0
|
|
923
|
+
|
|
924
|
+
async def cache_exists(self, key: str) -> bool:
|
|
925
|
+
"""Check if cache key exists and is not expired."""
|
|
926
|
+
import time
|
|
927
|
+
try:
|
|
928
|
+
async with self.get_session() as session:
|
|
929
|
+
stmt = select(CacheEntry).where(CacheEntry.key == key)
|
|
930
|
+
result = await session.execute(stmt)
|
|
931
|
+
entry = result.scalar_one_or_none()
|
|
932
|
+
|
|
933
|
+
if not entry:
|
|
934
|
+
return False
|
|
935
|
+
|
|
936
|
+
# Check expiration
|
|
937
|
+
if entry.expires_at and entry.expires_at < time.time():
|
|
938
|
+
return False
|
|
939
|
+
|
|
940
|
+
return True
|
|
941
|
+
|
|
942
|
+
except Exception as e:
|
|
943
|
+
logger.error("Failed to check cache exists", key=key, error=str(e))
|
|
944
|
+
return False
|
|
945
|
+
|
|
946
|
+
# ============================================================================
|
|
947
|
+
# Tool Schemas (Source of truth for tool node configurations)
|
|
948
|
+
# ============================================================================
|
|
949
|
+
|
|
950
|
+
async def save_tool_schema(self, node_id: str, tool_name: str, tool_description: str,
|
|
951
|
+
schema_config: Dict[str, Any],
|
|
952
|
+
connected_services: Optional[Dict[str, Any]] = None) -> bool:
|
|
953
|
+
"""Save or update tool schema for a node."""
|
|
954
|
+
try:
|
|
955
|
+
async with self.get_session() as session:
|
|
956
|
+
# Try to get existing schema
|
|
957
|
+
stmt = select(ToolSchema).where(ToolSchema.node_id == node_id)
|
|
958
|
+
result = await session.execute(stmt)
|
|
959
|
+
existing = result.scalar_one_or_none()
|
|
960
|
+
|
|
961
|
+
action = "updated"
|
|
962
|
+
if existing:
|
|
963
|
+
existing.tool_name = tool_name
|
|
964
|
+
existing.tool_description = tool_description
|
|
965
|
+
existing.schema_config = schema_config
|
|
966
|
+
existing.connected_services = connected_services
|
|
967
|
+
else:
|
|
968
|
+
action = "created"
|
|
969
|
+
existing = ToolSchema(
|
|
970
|
+
node_id=node_id,
|
|
971
|
+
tool_name=tool_name,
|
|
972
|
+
tool_description=tool_description,
|
|
973
|
+
schema_config=schema_config,
|
|
974
|
+
connected_services=connected_services
|
|
975
|
+
)
|
|
976
|
+
session.add(existing)
|
|
977
|
+
|
|
978
|
+
await session.commit()
|
|
979
|
+
logger.info(f"[DB] Tool schema {action}", node_id=node_id, tool_name=tool_name)
|
|
980
|
+
return True
|
|
981
|
+
|
|
982
|
+
except Exception as e:
|
|
983
|
+
logger.error("Failed to save tool schema", node_id=node_id, error=str(e))
|
|
984
|
+
return False
|
|
985
|
+
|
|
986
|
+
async def get_tool_schema(self, node_id: str) -> Optional[Dict[str, Any]]:
|
|
987
|
+
"""Get tool schema for a node."""
|
|
988
|
+
try:
|
|
989
|
+
async with self.get_session() as session:
|
|
990
|
+
stmt = select(ToolSchema).where(ToolSchema.node_id == node_id)
|
|
991
|
+
result = await session.execute(stmt)
|
|
992
|
+
schema = result.scalar_one_or_none()
|
|
993
|
+
|
|
994
|
+
if not schema:
|
|
995
|
+
return None
|
|
996
|
+
|
|
997
|
+
return {
|
|
998
|
+
"node_id": schema.node_id,
|
|
999
|
+
"tool_name": schema.tool_name,
|
|
1000
|
+
"tool_description": schema.tool_description,
|
|
1001
|
+
"schema_config": schema.schema_config,
|
|
1002
|
+
"connected_services": schema.connected_services,
|
|
1003
|
+
"created_at": schema.created_at.isoformat() if schema.created_at else None,
|
|
1004
|
+
"updated_at": schema.updated_at.isoformat() if schema.updated_at else None
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
except Exception as e:
|
|
1008
|
+
logger.error("Failed to get tool schema", node_id=node_id, error=str(e))
|
|
1009
|
+
return None
|
|
1010
|
+
|
|
1011
|
+
async def delete_tool_schema(self, node_id: str) -> bool:
|
|
1012
|
+
"""Delete tool schema for a node."""
|
|
1013
|
+
try:
|
|
1014
|
+
async with self.get_session() as session:
|
|
1015
|
+
stmt = select(ToolSchema).where(ToolSchema.node_id == node_id)
|
|
1016
|
+
result = await session.execute(stmt)
|
|
1017
|
+
schema = result.scalar_one_or_none()
|
|
1018
|
+
|
|
1019
|
+
if schema:
|
|
1020
|
+
await session.delete(schema)
|
|
1021
|
+
await session.commit()
|
|
1022
|
+
logger.info("[DB] Tool schema deleted", node_id=node_id)
|
|
1023
|
+
|
|
1024
|
+
return True
|
|
1025
|
+
|
|
1026
|
+
except Exception as e:
|
|
1027
|
+
logger.error("Failed to delete tool schema", node_id=node_id, error=str(e))
|
|
1028
|
+
return False
|
|
1029
|
+
|
|
1030
|
+
async def get_all_tool_schemas(self) -> List[Dict[str, Any]]:
|
|
1031
|
+
"""Get all tool schemas."""
|
|
1032
|
+
try:
|
|
1033
|
+
async with self.get_session() as session:
|
|
1034
|
+
stmt = select(ToolSchema).order_by(ToolSchema.updated_at.desc())
|
|
1035
|
+
result = await session.execute(stmt)
|
|
1036
|
+
schemas = result.scalars().all()
|
|
1037
|
+
|
|
1038
|
+
return [
|
|
1039
|
+
{
|
|
1040
|
+
"node_id": s.node_id,
|
|
1041
|
+
"tool_name": s.tool_name,
|
|
1042
|
+
"tool_description": s.tool_description,
|
|
1043
|
+
"schema_config": s.schema_config,
|
|
1044
|
+
"connected_services": s.connected_services,
|
|
1045
|
+
"updated_at": s.updated_at.isoformat() if s.updated_at else None
|
|
1046
|
+
}
|
|
1047
|
+
for s in schemas
|
|
1048
|
+
]
|
|
1049
|
+
|
|
1050
|
+
except Exception as e:
|
|
1051
|
+
logger.error("Failed to get all tool schemas", error=str(e))
|
|
1052
|
+
return []
|
|
1053
|
+
|
|
1054
|
+
# ============================================================================
|
|
1055
|
+
# Android Relay Session Persistence
|
|
1056
|
+
# ============================================================================
|
|
1057
|
+
|
|
1058
|
+
async def save_android_relay_session(
|
|
1059
|
+
self,
|
|
1060
|
+
relay_url: str,
|
|
1061
|
+
api_key: str,
|
|
1062
|
+
device_id: str,
|
|
1063
|
+
device_name: Optional[str] = None,
|
|
1064
|
+
session_token: Optional[str] = None
|
|
1065
|
+
) -> bool:
|
|
1066
|
+
"""Save Android relay pairing session for auto-reconnect on server restart.
|
|
1067
|
+
|
|
1068
|
+
Args:
|
|
1069
|
+
relay_url: WebSocket relay URL
|
|
1070
|
+
api_key: API key for relay authentication
|
|
1071
|
+
device_id: Paired Android device ID
|
|
1072
|
+
device_name: Paired device name
|
|
1073
|
+
session_token: Relay session token
|
|
1074
|
+
"""
|
|
1075
|
+
import json
|
|
1076
|
+
try:
|
|
1077
|
+
session_data = json.dumps({
|
|
1078
|
+
"relay_url": relay_url,
|
|
1079
|
+
"api_key": api_key,
|
|
1080
|
+
"device_id": device_id,
|
|
1081
|
+
"device_name": device_name,
|
|
1082
|
+
"session_token": session_token
|
|
1083
|
+
})
|
|
1084
|
+
# No TTL - session persists until explicitly cleared
|
|
1085
|
+
return await self.set_cache_entry("android_relay_session", session_data)
|
|
1086
|
+
except Exception as e:
|
|
1087
|
+
logger.error("Failed to save Android relay session", error=str(e))
|
|
1088
|
+
return False
|
|
1089
|
+
|
|
1090
|
+
async def get_android_relay_session(self) -> Optional[Dict[str, Any]]:
|
|
1091
|
+
"""Get stored Android relay session for auto-reconnect.
|
|
1092
|
+
|
|
1093
|
+
Returns:
|
|
1094
|
+
Session data dict or None if not found
|
|
1095
|
+
"""
|
|
1096
|
+
import json
|
|
1097
|
+
try:
|
|
1098
|
+
value = await self.get_cache_entry("android_relay_session")
|
|
1099
|
+
if value:
|
|
1100
|
+
return json.loads(value)
|
|
1101
|
+
return None
|
|
1102
|
+
except Exception as e:
|
|
1103
|
+
logger.error("Failed to get Android relay session", error=str(e))
|
|
1104
|
+
return None
|
|
1105
|
+
|
|
1106
|
+
async def clear_android_relay_session(self) -> bool:
|
|
1107
|
+
"""Clear stored Android relay session (on explicit disconnect)."""
|
|
1108
|
+
try:
|
|
1109
|
+
return await self.delete_cache_entry("android_relay_session")
|
|
1110
|
+
except Exception as e:
|
|
1111
|
+
logger.error("Failed to clear Android relay session", error=str(e))
|
|
1112
|
+
return False
|
|
1113
|
+
|
|
1114
|
+
# ============================================================================
|
|
1115
|
+
# User Skills (Custom skills for Zeenie)
|
|
1116
|
+
# ============================================================================
|
|
1117
|
+
|
|
1118
|
+
async def create_user_skill(
|
|
1119
|
+
self,
|
|
1120
|
+
name: str,
|
|
1121
|
+
display_name: str,
|
|
1122
|
+
description: str,
|
|
1123
|
+
instructions: str,
|
|
1124
|
+
allowed_tools: Optional[str] = None,
|
|
1125
|
+
category: str = "custom",
|
|
1126
|
+
icon: str = "star",
|
|
1127
|
+
color: str = "#6366F1",
|
|
1128
|
+
metadata_json: Optional[Dict[str, Any]] = None,
|
|
1129
|
+
created_by: Optional[int] = None
|
|
1130
|
+
) -> Optional[Dict[str, Any]]:
|
|
1131
|
+
"""Create a new user skill."""
|
|
1132
|
+
try:
|
|
1133
|
+
async with self.get_session() as session:
|
|
1134
|
+
skill = UserSkill(
|
|
1135
|
+
name=name,
|
|
1136
|
+
display_name=display_name,
|
|
1137
|
+
description=description,
|
|
1138
|
+
instructions=instructions,
|
|
1139
|
+
allowed_tools=allowed_tools,
|
|
1140
|
+
category=category,
|
|
1141
|
+
icon=icon,
|
|
1142
|
+
color=color,
|
|
1143
|
+
metadata_json=metadata_json,
|
|
1144
|
+
created_by=created_by
|
|
1145
|
+
)
|
|
1146
|
+
session.add(skill)
|
|
1147
|
+
await session.commit()
|
|
1148
|
+
await session.refresh(skill)
|
|
1149
|
+
|
|
1150
|
+
logger.info(f"[DB] Created user skill: {name}")
|
|
1151
|
+
return self._skill_to_dict(skill)
|
|
1152
|
+
|
|
1153
|
+
except IntegrityError:
|
|
1154
|
+
logger.error(f"User skill with name '{name}' already exists")
|
|
1155
|
+
return None
|
|
1156
|
+
except Exception as e:
|
|
1157
|
+
logger.error("Failed to create user skill", name=name, error=str(e))
|
|
1158
|
+
return None
|
|
1159
|
+
|
|
1160
|
+
async def get_user_skill(self, name: str) -> Optional[Dict[str, Any]]:
|
|
1161
|
+
"""Get user skill by name."""
|
|
1162
|
+
try:
|
|
1163
|
+
async with self.get_session() as session:
|
|
1164
|
+
stmt = select(UserSkill).where(UserSkill.name == name)
|
|
1165
|
+
result = await session.execute(stmt)
|
|
1166
|
+
skill = result.scalar_one_or_none()
|
|
1167
|
+
|
|
1168
|
+
return self._skill_to_dict(skill) if skill else None
|
|
1169
|
+
|
|
1170
|
+
except Exception as e:
|
|
1171
|
+
logger.error("Failed to get user skill", name=name, error=str(e))
|
|
1172
|
+
return None
|
|
1173
|
+
|
|
1174
|
+
async def get_user_skill_by_id(self, skill_id: int) -> Optional[Dict[str, Any]]:
|
|
1175
|
+
"""Get user skill by ID."""
|
|
1176
|
+
try:
|
|
1177
|
+
async with self.get_session() as session:
|
|
1178
|
+
stmt = select(UserSkill).where(UserSkill.id == skill_id)
|
|
1179
|
+
result = await session.execute(stmt)
|
|
1180
|
+
skill = result.scalar_one_or_none()
|
|
1181
|
+
|
|
1182
|
+
return self._skill_to_dict(skill) if skill else None
|
|
1183
|
+
|
|
1184
|
+
except Exception as e:
|
|
1185
|
+
logger.error("Failed to get user skill by id", skill_id=skill_id, error=str(e))
|
|
1186
|
+
return None
|
|
1187
|
+
|
|
1188
|
+
async def get_all_user_skills(self, active_only: bool = True) -> List[Dict[str, Any]]:
|
|
1189
|
+
"""Get all user skills, optionally filtered by active status."""
|
|
1190
|
+
try:
|
|
1191
|
+
async with self.get_session() as session:
|
|
1192
|
+
if active_only:
|
|
1193
|
+
stmt = select(UserSkill).where(UserSkill.is_active == True).order_by(UserSkill.display_name)
|
|
1194
|
+
else:
|
|
1195
|
+
stmt = select(UserSkill).order_by(UserSkill.display_name)
|
|
1196
|
+
|
|
1197
|
+
result = await session.execute(stmt)
|
|
1198
|
+
skills = result.scalars().all()
|
|
1199
|
+
|
|
1200
|
+
return [self._skill_to_dict(s) for s in skills]
|
|
1201
|
+
|
|
1202
|
+
except Exception as e:
|
|
1203
|
+
logger.error("Failed to get all user skills", error=str(e))
|
|
1204
|
+
return []
|
|
1205
|
+
|
|
1206
|
+
async def update_user_skill(
|
|
1207
|
+
self,
|
|
1208
|
+
name: str,
|
|
1209
|
+
display_name: Optional[str] = None,
|
|
1210
|
+
description: Optional[str] = None,
|
|
1211
|
+
instructions: Optional[str] = None,
|
|
1212
|
+
allowed_tools: Optional[str] = None,
|
|
1213
|
+
category: Optional[str] = None,
|
|
1214
|
+
icon: Optional[str] = None,
|
|
1215
|
+
color: Optional[str] = None,
|
|
1216
|
+
metadata_json: Optional[Dict[str, Any]] = None,
|
|
1217
|
+
is_active: Optional[bool] = None
|
|
1218
|
+
) -> Optional[Dict[str, Any]]:
|
|
1219
|
+
"""Update an existing user skill."""
|
|
1220
|
+
try:
|
|
1221
|
+
async with self.get_session() as session:
|
|
1222
|
+
stmt = select(UserSkill).where(UserSkill.name == name)
|
|
1223
|
+
result = await session.execute(stmt)
|
|
1224
|
+
skill = result.scalar_one_or_none()
|
|
1225
|
+
|
|
1226
|
+
if not skill:
|
|
1227
|
+
logger.error(f"User skill '{name}' not found for update")
|
|
1228
|
+
return None
|
|
1229
|
+
|
|
1230
|
+
# Update only provided fields
|
|
1231
|
+
if display_name is not None:
|
|
1232
|
+
skill.display_name = display_name
|
|
1233
|
+
if description is not None:
|
|
1234
|
+
skill.description = description
|
|
1235
|
+
if instructions is not None:
|
|
1236
|
+
skill.instructions = instructions
|
|
1237
|
+
if allowed_tools is not None:
|
|
1238
|
+
skill.allowed_tools = allowed_tools
|
|
1239
|
+
if category is not None:
|
|
1240
|
+
skill.category = category
|
|
1241
|
+
if icon is not None:
|
|
1242
|
+
skill.icon = icon
|
|
1243
|
+
if color is not None:
|
|
1244
|
+
skill.color = color
|
|
1245
|
+
if metadata_json is not None:
|
|
1246
|
+
skill.metadata_json = metadata_json
|
|
1247
|
+
if is_active is not None:
|
|
1248
|
+
skill.is_active = is_active
|
|
1249
|
+
|
|
1250
|
+
await session.commit()
|
|
1251
|
+
await session.refresh(skill)
|
|
1252
|
+
|
|
1253
|
+
logger.info(f"[DB] Updated user skill: {name}")
|
|
1254
|
+
return self._skill_to_dict(skill)
|
|
1255
|
+
|
|
1256
|
+
except Exception as e:
|
|
1257
|
+
logger.error("Failed to update user skill", name=name, error=str(e))
|
|
1258
|
+
return None
|
|
1259
|
+
|
|
1260
|
+
async def delete_user_skill(self, name: str) -> bool:
|
|
1261
|
+
"""Delete a user skill by name."""
|
|
1262
|
+
try:
|
|
1263
|
+
async with self.get_session() as session:
|
|
1264
|
+
stmt = select(UserSkill).where(UserSkill.name == name)
|
|
1265
|
+
result = await session.execute(stmt)
|
|
1266
|
+
skill = result.scalar_one_or_none()
|
|
1267
|
+
|
|
1268
|
+
if skill:
|
|
1269
|
+
await session.delete(skill)
|
|
1270
|
+
await session.commit()
|
|
1271
|
+
logger.info(f"[DB] Deleted user skill: {name}")
|
|
1272
|
+
return True
|
|
1273
|
+
|
|
1274
|
+
return False
|
|
1275
|
+
|
|
1276
|
+
except Exception as e:
|
|
1277
|
+
logger.error("Failed to delete user skill", name=name, error=str(e))
|
|
1278
|
+
return False
|
|
1279
|
+
|
|
1280
|
+
def _skill_to_dict(self, skill: UserSkill) -> Dict[str, Any]:
|
|
1281
|
+
"""Convert UserSkill model to dictionary."""
|
|
1282
|
+
return {
|
|
1283
|
+
"id": skill.id,
|
|
1284
|
+
"name": skill.name,
|
|
1285
|
+
"display_name": skill.display_name,
|
|
1286
|
+
"description": skill.description,
|
|
1287
|
+
"instructions": skill.instructions,
|
|
1288
|
+
"allowed_tools": skill.allowed_tools.split(",") if skill.allowed_tools else [],
|
|
1289
|
+
"category": skill.category,
|
|
1290
|
+
"icon": skill.icon,
|
|
1291
|
+
"color": skill.color,
|
|
1292
|
+
"metadata": skill.metadata_json,
|
|
1293
|
+
"is_active": skill.is_active,
|
|
1294
|
+
"created_by": skill.created_by,
|
|
1295
|
+
"created_at": skill.created_at.isoformat() if skill.created_at else None,
|
|
1296
|
+
"updated_at": skill.updated_at.isoformat() if skill.updated_at else None
|
|
1211
1297
|
}
|