machinaos 0.0.76 → 0.0.78
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/README.md +143 -107
- package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
- package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
- package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
- package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
- package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
- package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
- package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
- package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
- package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
- package/client/dist/assets/index-DQ0nwhec.js +257 -0
- package/client/dist/assets/index-DxmbVskS.css +1 -0
- package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
- package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
- package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
- package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
- package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
- package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
- package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
- package/client/dist/index.html +37 -3
- package/client/index.html +28 -1
- package/client/package.json +44 -40
- package/client/src/App.tsx +2 -0
- package/client/src/Dashboard.tsx +157 -45
- package/client/src/ParameterPanel.tsx +3 -5
- package/client/src/adapters/nodeSpecToDescription.ts +1 -0
- package/client/src/assets/icons/NodeIcon.tsx +32 -0
- package/client/src/assets/icons/index.ts +4 -0
- package/client/src/assets/icons/stripe.svg +1 -0
- package/client/src/assets/icons/themedGlyphs.ts +404 -0
- package/client/src/components/AIAgentNode.tsx +77 -53
- package/client/src/components/GenericNode.tsx +34 -52
- package/client/src/components/OutputPanel.tsx +64 -147
- package/client/src/components/ParameterRenderer.tsx +5 -3
- package/client/src/components/SkillEditorModal.tsx +9 -18
- package/client/src/components/SquareNode.tsx +97 -115
- package/client/src/components/StartNode.tsx +32 -42
- package/client/src/components/SvgFilterDefs.tsx +54 -0
- package/client/src/components/TeamMonitorNode.tsx +12 -14
- package/client/src/components/ToolkitNode.tsx +35 -60
- package/client/src/components/TriggerNode.tsx +43 -77
- package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
- package/client/src/components/credentials/CredentialsModal.tsx +98 -30
- package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
- package/client/src/components/credentials/catalogueAdapter.ts +17 -1
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
- package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
- package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
- package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
- package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
- package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
- package/client/src/components/credentials/types.ts +12 -2
- package/client/src/components/credentials/useCredentialPanel.ts +43 -19
- package/client/src/components/icons/AIProviderIcons.tsx +16 -0
- package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
- package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
- package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
- package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
- package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
- package/client/src/components/parameterPanel/InputSection.tsx +9 -7
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
- package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
- package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
- package/client/src/components/ui/AIResultModal.tsx +1 -1
- package/client/src/components/ui/CollapsibleSection.tsx +9 -5
- package/client/src/components/ui/CommandPalette.tsx +147 -0
- package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
- package/client/src/components/ui/ComponentItem.tsx +13 -7
- package/client/src/components/ui/ComponentPalette.tsx +24 -13
- package/client/src/components/ui/ConsolePanel.tsx +19 -11
- package/client/src/components/ui/DropCap.tsx +28 -0
- package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
- package/client/src/components/ui/InputNodesPanel.tsx +1 -1
- package/client/src/components/ui/Modal.tsx +38 -6
- package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +42 -13
- package/client/src/components/ui/StatusBar.tsx +108 -0
- package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
- package/client/src/components/ui/TopToolbar.tsx +42 -25
- package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
- package/client/src/components/ui/action-button.tsx +40 -15
- package/client/src/components/ui/button.tsx +24 -1
- package/client/src/components/ui/dropdown-menu.tsx +24 -2
- package/client/src/components/ui/input.tsx +19 -2
- package/client/src/components/ui/select.tsx +15 -0
- package/client/src/components/ui/textarea.tsx +15 -2
- package/client/src/contexts/AuthContext.tsx +148 -109
- package/client/src/contexts/ThemeContext.tsx +93 -17
- package/client/src/contexts/WebSocketContext.tsx +373 -206
- package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
- package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
- package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
- package/client/src/hooks/useAppTheme.ts +209 -7
- package/client/src/hooks/useAutoSkillEdges.ts +7 -2
- package/client/src/hooks/useCatalogueQuery.ts +67 -1
- package/client/src/hooks/useDragVariable.ts +1 -1
- package/client/src/hooks/useNodeAllowlist.ts +115 -8
- package/client/src/hooks/useOnboarding.ts +20 -8
- package/client/src/hooks/useParameterPanel.ts +2 -1
- package/client/src/hooks/useReactFlowNodes.ts +2 -1
- package/client/src/hooks/useSound.ts +185 -0
- package/client/src/hooks/useWorkflowManagement.ts +6 -8
- package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
- package/client/src/index.css +65 -3
- package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
- package/client/src/lib/aiModelProviders.ts +8 -0
- package/client/src/lib/connectionConfig.ts +107 -0
- package/client/src/lib/queryPersist.ts +13 -5
- package/client/src/lib/sound.ts +393 -0
- package/client/src/main.tsx +20 -0
- package/client/src/store/useAppStore.ts +26 -0
- package/client/src/styles/canvasAnimations.ts +37 -36
- package/client/src/styles/theme.ts +36 -20
- package/client/src/test/setup.ts +1 -0
- package/client/src/themes/atomic.css +253 -0
- package/client/src/themes/base.css +373 -0
- package/client/src/themes/cyber.css +890 -0
- package/client/src/themes/dark.css +70 -0
- package/client/src/themes/edo.css +246 -0
- package/client/src/themes/greek.css +293 -0
- package/client/src/themes/light.css +78 -0
- package/client/src/themes/plague.css +253 -0
- package/client/src/themes/renaissance.css +727 -0
- package/client/src/themes/rot.css +249 -0
- package/client/src/themes/steampunk.css +272 -0
- package/client/src/themes/surveillance.css +289 -0
- package/client/src/themes/wasteland.css +250 -0
- package/client/src/types/INodeProperties.ts +5 -0
- package/client/src/types/NodeTypes.ts +11 -1
- package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
- package/client/src/types/cloudEvents.ts +78 -0
- package/client/src/vite-env.d.ts +7 -0
- package/client/tsconfig.json +1 -1
- package/client/vite.config.js +62 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/machina/commands/build.py +51 -7
- package/machina/pyproject.toml +4 -0
- package/machina/supervisor.py +12 -2
- package/machina/tree.py +71 -21
- package/package.json +4 -4
- package/scripts/install.js +16 -1
- package/server/config/ai_cli_providers.json +54 -0
- package/server/config/credential_providers.json +109 -2
- package/server/config/llm_defaults.json +24 -0
- package/server/config/model_registry.json +338 -499
- package/server/config/node_allowlist.json +16 -1
- package/server/config/pricing.json +8 -0
- package/server/constants.py +38 -15
- package/server/core/container.py +2 -2
- package/server/core/credentials_database.py +35 -2
- package/server/core/logging.py +4 -3
- package/server/main.py +99 -13
- package/server/models/node_metadata.py +1 -0
- package/server/nodejs/package.json +8 -6
- package/server/nodejs/src/index.ts +22 -5
- package/server/nodes/README.md +31 -4
- package/server/nodes/agent/_inline.py +2 -0
- package/server/nodes/agent/_specialized.py +6 -3
- package/server/nodes/agent/ai_agent.py +13 -3
- package/server/nodes/agent/chat_agent.py +6 -3
- package/server/nodes/agent/claude_code_agent.py +287 -75
- package/server/nodes/agent/codex_agent.py +239 -0
- package/server/nodes/agent/deep_agent.py +3 -3
- package/server/nodes/agent/rlm_agent.py +3 -3
- package/server/nodes/android/__init__.py +31 -1
- package/server/nodes/android/_base.py +9 -5
- package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
- package/server/nodes/android/_handlers.py +154 -0
- package/server/nodes/android/_option_loaders.py +44 -0
- package/server/nodes/android/_refresh.py +127 -0
- package/server/{services/android → nodes/android/_relay}/client.py +4 -4
- package/server/{routers/android.py → nodes/android/_router.py} +27 -8
- package/server/nodes/browser/browser.py +2 -2
- package/server/nodes/code/_base.py +6 -2
- package/server/nodes/code/_claude_code.py +134 -0
- package/server/nodes/document/embedding_generator.py +3 -3
- package/server/nodes/document/http_scraper.py +3 -3
- package/server/nodes/document/vector_store.py +5 -5
- package/server/nodes/email/__init__.py +11 -1
- package/server/nodes/email/_filters.py +21 -0
- package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
- package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
- package/server/nodes/email/email_read.py +1 -1
- package/server/nodes/email/email_receive.py +54 -5
- package/server/nodes/email/email_send.py +1 -1
- package/server/nodes/filesystem/shell.py +24 -1
- package/server/nodes/google/__init__.py +55 -1
- package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
- package/server/nodes/google/_base.py +2 -2
- package/server/nodes/google/_credentials.py +5 -5
- package/server/nodes/google/_filters.py +25 -0
- package/server/nodes/google/_handlers.py +57 -0
- package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
- package/server/nodes/google/_option_loaders.py +107 -0
- package/server/nodes/google/_refresh.py +66 -0
- package/server/nodes/google/_router.py +131 -0
- package/server/nodes/google/gmail_receive.py +41 -4
- package/server/nodes/groups.py +1 -0
- package/server/nodes/location/_credentials.py +45 -1
- package/server/{services/maps.py → nodes/location/_service.py} +18 -3
- package/server/nodes/location/gmaps_create.py +4 -4
- package/server/nodes/location/gmaps_locations.py +4 -4
- package/server/nodes/location/gmaps_nearby_places.py +4 -4
- package/server/nodes/model/_base.py +8 -3
- package/server/nodes/model/_credentials.py +96 -8
- package/server/nodes/model/_local_validator.py +345 -0
- package/server/nodes/model/lmstudio_chat_model.py +23 -0
- package/server/nodes/model/ollama_chat_model.py +25 -0
- package/server/nodes/proxy/_usage.py +2 -2
- package/server/nodes/proxy/proxy_config.py +14 -14
- package/server/nodes/proxy/proxy_request.py +4 -4
- package/server/nodes/scraper/_credentials.py +29 -1
- package/server/nodes/scraper/apify_actor.py +9 -9
- package/server/nodes/scraper/crawlee_scraper.py +5 -5
- package/server/nodes/search/brave_search.py +4 -0
- package/server/nodes/search/perplexity_search.py +9 -0
- package/server/nodes/search/serper_search.py +3 -0
- package/server/nodes/skill/simple_memory.py +12 -0
- package/server/nodes/social/_base.py +2 -2
- package/server/nodes/stripe/__init__.py +46 -0
- package/server/nodes/stripe/_credentials.py +33 -0
- package/server/nodes/stripe/_handlers.py +270 -0
- package/server/nodes/stripe/_install.py +127 -0
- package/server/nodes/stripe/_source.py +174 -0
- package/server/nodes/stripe/stripe_action.py +81 -0
- package/server/nodes/stripe/stripe_receive.py +92 -0
- package/server/nodes/telegram/_credentials.py +52 -1
- package/server/nodes/telegram/_handlers.py +19 -18
- package/server/nodes/telegram/_service.py +134 -32
- package/server/nodes/telegram/telegram_send.py +5 -6
- package/server/nodes/text/file_handler.py +2 -2
- package/server/nodes/text/text_generator.py +2 -2
- package/server/nodes/tool/agent_builder.py +630 -0
- package/server/nodes/tool/task_manager.py +144 -2
- package/server/nodes/twitter/__init__.py +38 -1
- package/server/nodes/twitter/_base.py +7 -7
- package/server/nodes/twitter/_credentials.py +1 -1
- package/server/nodes/twitter/_filters.py +37 -0
- package/server/nodes/twitter/_handlers.py +77 -0
- package/server/nodes/twitter/_oauth.py +124 -0
- package/server/nodes/twitter/_refresh.py +78 -0
- package/server/nodes/twitter/_router.py +29 -0
- package/server/nodes/twitter/twitter_receive.py +4 -0
- package/server/nodes/visuals.json +64 -19
- package/server/nodes/whatsapp/__init__.py +45 -5
- package/server/nodes/whatsapp/_base.py +3 -3
- package/server/nodes/whatsapp/_filters.py +137 -0
- package/server/nodes/whatsapp/_handlers.py +167 -0
- package/server/nodes/whatsapp/_option_loaders.py +68 -0
- package/server/nodes/whatsapp/_refresh.py +62 -0
- package/server/nodes/whatsapp/_runtime.py +1 -1
- package/server/pyproject.toml +29 -7
- package/server/routers/schemas.py +2 -2
- package/server/routers/webhook.py +26 -9
- package/server/routers/websocket.py +149 -810
- package/server/services/ai.py +89 -8
- package/server/services/auth.py +220 -43
- package/server/services/claude_oauth.py +126 -100
- package/server/services/cli_agent/__init__.py +78 -0
- package/server/services/cli_agent/_handlers.py +237 -0
- package/server/services/cli_agent/config.py +112 -0
- package/server/services/cli_agent/factory.py +48 -0
- package/server/services/cli_agent/lockfile.py +141 -0
- package/server/services/cli_agent/mcp_server.py +482 -0
- package/server/services/cli_agent/protocol.py +173 -0
- package/server/services/cli_agent/providers/__init__.py +9 -0
- package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
- package/server/services/cli_agent/providers/google_gemini.py +80 -0
- package/server/services/cli_agent/providers/openai_codex.py +310 -0
- package/server/services/cli_agent/service.py +607 -0
- package/server/services/cli_agent/session.py +618 -0
- package/server/services/cli_agent/types.py +227 -0
- package/server/services/cli_agent/workflow_tools.py +233 -0
- package/server/services/credential_registry.py +26 -1
- package/server/services/deployment/manager.py +26 -145
- package/server/services/deployment/poll_registry.py +59 -0
- package/server/services/event_waiter.py +76 -246
- package/server/services/events/__init__.py +54 -0
- package/server/services/events/cli.py +78 -0
- package/server/services/events/daemon.py +163 -0
- package/server/services/events/envelope.py +281 -0
- package/server/services/events/lifecycle.py +99 -0
- package/server/services/events/oauth_lifecycle.py +534 -0
- package/server/services/events/polling.py +60 -0
- package/server/services/events/push.py +36 -0
- package/server/services/events/source.py +63 -0
- package/server/services/events/triggers.py +118 -0
- package/server/services/events/verifiers/__init__.py +25 -0
- package/server/services/events/verifiers/base.py +28 -0
- package/server/services/events/verifiers/github.py +25 -0
- package/server/services/events/verifiers/hmac_basic.py +32 -0
- package/server/services/events/verifiers/standard_webhooks.py +47 -0
- package/server/services/events/verifiers/stripe.py +42 -0
- package/server/services/events/webhook.py +105 -0
- package/server/services/handlers/tools.py +28 -186
- package/server/services/llm/config.py +7 -0
- package/server/services/llm/factory.py +8 -2
- package/server/services/memory/__init__.py +52 -0
- package/server/services/memory/jsonl.py +80 -0
- package/server/services/memory/markdown.py +65 -0
- package/server/services/memory/state.py +112 -0
- package/server/services/memory/vector_store.py +40 -0
- package/server/services/model_registry.py +76 -0
- package/server/services/node_allowlist.py +71 -15
- package/server/services/node_executor.py +2 -2
- package/server/services/node_output_schemas.py +21 -10
- package/server/services/node_spec.py +1 -1
- package/server/services/oauth_utils.py +1 -1
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +44 -2
- package/server/services/plugin/credential.py +288 -1
- package/server/services/plugin/deps.py +105 -0
- package/server/services/plugin/edge_walker.py +12 -4
- package/server/services/plugin/oauth.py +381 -0
- package/server/services/plugin/polling.py +247 -0
- package/server/services/plugin/registry.py +145 -0
- package/server/services/plugin/singleton.py +65 -0
- package/server/services/plugin/ws.py +81 -0
- package/server/services/process_service.py +31 -2
- package/server/services/status_broadcaster.py +155 -238
- package/server/services/temporal/workflow.py +7 -7
- package/server/services/workflow.py +21 -3
- package/server/services/ws_handler_registry.py +111 -28
- package/server/skills/GUIDE.md +16 -1
- package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
- package/server/tests/credentials/test_auth_service.py +16 -9
- package/server/tests/credentials/test_credential_broadcasts.py +219 -0
- package/server/tests/credentials/test_google_oauth.py +6 -6
- package/server/tests/credentials/test_oauth_utils.py +1 -1
- package/server/tests/credentials/test_twitter_oauth.py +2 -2
- package/server/tests/credentials/test_websocket_handlers.py +44 -20
- package/server/tests/llm/test_factory.py +1 -0
- package/server/tests/llm/test_wiring.py +5 -1
- package/server/tests/nodes/_compat.py +24 -24
- package/server/tests/nodes/test_agent_builder.py +439 -0
- package/server/tests/nodes/test_ai_tools.py +18 -14
- package/server/tests/nodes/test_code_fs_process.py +17 -8
- package/server/tests/nodes/test_email.py +10 -9
- package/server/tests/nodes/test_google_workspace.py +2 -2
- package/server/tests/nodes/test_specialized_agents.py +100 -53
- package/server/tests/nodes/test_stripe_plugin.py +293 -0
- package/server/tests/nodes/test_telegram_social.py +4 -4
- package/server/tests/nodes/test_twitter.py +1 -1
- package/server/tests/nodes/test_web_automation.py +2 -2
- package/server/tests/nodes/test_whatsapp.py +9 -9
- package/server/tests/services/cli_agent/__init__.py +0 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
- package/server/tests/services/cli_agent/test_providers.py +358 -0
- package/server/tests/services/cli_agent/test_service.py +298 -0
- package/server/tests/services/memory/__init__.py +0 -0
- package/server/tests/services/memory/test_jsonl.py +188 -0
- package/server/tests/services/test_events.py +333 -0
- package/server/tests/test_node_spec.py +56 -16
- package/server/tests/test_plugin_helpers.py +116 -0
- package/server/tests/test_plugin_self_containment.py +486 -0
- package/server/tests/test_status_broadcasts.py +425 -0
- package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
- package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
- package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
- package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
- package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
- package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
- package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
- package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
- package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
- package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
- package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
- package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
- package/client/dist/assets/index-D9tZfgvi.js +0 -363
- package/client/dist/assets/index-al7snTkG.css +0 -1
- package/client/src/components/credentials/providers.tsx +0 -177
- package/server/routers/google.py +0 -277
- package/server/routers/maps.py +0 -142
- package/server/routers/twitter.py +0 -365
- package/server/services/claude_code_service.py +0 -106
- package/server/services/memory.py +0 -159
- package/server/services/node_option_loaders/__init__.py +0 -77
- package/server/services/node_option_loaders/android_loaders.py +0 -55
- package/server/services/node_option_loaders/google_loaders.py +0 -97
- package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
- package/server/services/twitter_oauth.py +0 -411
- package/server/services/websocket_client.py +0 -29
- /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
- /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
- /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
- /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
|
@@ -13,7 +13,7 @@ from datetime import datetime
|
|
|
13
13
|
from typing import Dict, Any, List, Optional, Callable, TYPE_CHECKING
|
|
14
14
|
|
|
15
15
|
from core.logging import get_logger
|
|
16
|
-
from constants import
|
|
16
|
+
from constants import POLLING_TRIGGER_TYPES, TOOLKIT_NODE_TYPES, WORKFLOW_TRIGGER_TYPES
|
|
17
17
|
from services import event_waiter
|
|
18
18
|
from .state import DeploymentState, TriggerInfo
|
|
19
19
|
from .triggers import TriggerManager
|
|
@@ -418,17 +418,25 @@ class DeploymentManager:
|
|
|
418
418
|
if not trigger_manager:
|
|
419
419
|
raise RuntimeError(f"No trigger manager for workflow {workflow_id}")
|
|
420
420
|
|
|
421
|
-
# Polling triggers need active API polling instead of event_waiter
|
|
421
|
+
# Polling triggers need active API polling instead of event_waiter.
|
|
422
|
+
# Plugins register a factory via
|
|
423
|
+
# ``services.plugin.PollingTriggerNode.__init_subclass__`` →
|
|
424
|
+
# ``services.deployment.poll_registry.register_poll_coroutine_factory``.
|
|
422
425
|
if node_type in POLLING_TRIGGER_TYPES:
|
|
423
|
-
|
|
424
|
-
|
|
426
|
+
from services.deployment.poll_registry import (
|
|
427
|
+
get_poll_coroutine_factory,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
factory = get_poll_coroutine_factory(node_type)
|
|
431
|
+
if factory is not None:
|
|
432
|
+
poll_coroutine = factory(node_id, params)
|
|
425
433
|
await trigger_manager.setup_polling_trigger(
|
|
426
434
|
node_id, node_type, params, poll_coroutine, on_event,
|
|
427
435
|
self._broadcaster, workflow_id=workflow_id
|
|
428
436
|
)
|
|
429
437
|
return TriggerInfo(node_id, node_type)
|
|
430
|
-
# Fall through to event_waiter if no polling
|
|
431
|
-
logger.warning("No polling
|
|
438
|
+
# Fall through to event_waiter if no polling factory registered
|
|
439
|
+
logger.warning("No polling factory registered for trigger", node_type=node_type)
|
|
432
440
|
|
|
433
441
|
await trigger_manager.setup_event_trigger(
|
|
434
442
|
node_id, node_type, params, on_event, self._broadcaster,
|
|
@@ -641,10 +649,12 @@ class DeploymentManager:
|
|
|
641
649
|
continue
|
|
642
650
|
downstream_ids.add(source)
|
|
643
651
|
|
|
644
|
-
# Include sub-nodes connected to toolkit nodes (n8n Sub-Node pattern)
|
|
645
|
-
#
|
|
646
|
-
#
|
|
647
|
-
|
|
652
|
+
# Include sub-nodes connected to toolkit nodes (n8n Sub-Node pattern).
|
|
653
|
+
# Service nodes connect to a toolkit's input-main (not a config
|
|
654
|
+
# handle) and need to be included so the toolkit can discover
|
|
655
|
+
# them. ``TOOLKIT_NODE_TYPES`` is the canonical set; today only
|
|
656
|
+
# ``androidTool`` is in it.
|
|
657
|
+
toolkit_node_ids = {n['id'] for n in nodes if n.get('type') in TOOLKIT_NODE_TYPES and n['id'] in downstream_ids}
|
|
648
658
|
for edge in edges:
|
|
649
659
|
target = edge.get('target')
|
|
650
660
|
source = edge.get('source')
|
|
@@ -677,141 +687,12 @@ class DeploymentManager:
|
|
|
677
687
|
# POLLING TRIGGER FACTORIES
|
|
678
688
|
# =========================================================================
|
|
679
689
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if node_type == 'gmailReceive':
|
|
687
|
-
return self._create_gmail_poll_coroutine(node_id, params)
|
|
688
|
-
elif node_type == 'emailReceive':
|
|
689
|
-
return self._create_email_poll_coroutine(node_id, params)
|
|
690
|
-
elif node_type == 'twitterReceive':
|
|
691
|
-
logger.warning("Twitter polling trigger not yet implemented for deployment")
|
|
692
|
-
return None
|
|
693
|
-
return None
|
|
694
|
-
|
|
695
|
-
def _create_gmail_poll_coroutine(self, node_id: str,
|
|
696
|
-
params: Dict[str, Any]) -> Callable:
|
|
697
|
-
"""Create Gmail API polling coroutine for deployment mode.
|
|
698
|
-
|
|
699
|
-
Polls Gmail API at configured intervals for new emails matching the filter.
|
|
700
|
-
Establishes a baseline of existing emails to avoid triggering on old messages.
|
|
701
|
-
"""
|
|
702
|
-
async def poll(queue: asyncio.Queue, is_running_fn: Callable):
|
|
703
|
-
from nodes.google._base import build_google_service
|
|
704
|
-
from nodes.google._gmail import (
|
|
705
|
-
poll_gmail_ids as _poll_gmail_ids,
|
|
706
|
-
fetch_email_details as _fetch_email_details,
|
|
707
|
-
mark_email_as_read as _mark_email_as_read,
|
|
708
|
-
)
|
|
709
|
-
|
|
710
|
-
# Authenticate with Gmail API
|
|
711
|
-
service = await build_google_service("gmail", "v1", params, {})
|
|
712
|
-
|
|
713
|
-
poll_interval = max(10, min(3600, params.get('poll_interval', 60)))
|
|
714
|
-
filter_query = params.get('filter_query', 'is:unread')
|
|
715
|
-
label_filter = params.get('label_filter', 'INBOX')
|
|
716
|
-
mark_as_read = params.get('mark_as_read', False)
|
|
717
|
-
|
|
718
|
-
# Build Gmail query
|
|
719
|
-
query = filter_query
|
|
720
|
-
if label_filter and label_filter != 'all':
|
|
721
|
-
query = f"label:{label_filter} {query}"
|
|
722
|
-
|
|
723
|
-
logger.info("Gmail poller starting",
|
|
724
|
-
node_id=node_id, query=query,
|
|
725
|
-
poll_interval=poll_interval)
|
|
726
|
-
|
|
727
|
-
# Establish baseline (avoid triggering on existing emails)
|
|
728
|
-
seen_ids: set = set()
|
|
729
|
-
try:
|
|
730
|
-
baseline = await _poll_gmail_ids(service, query)
|
|
731
|
-
seen_ids.update(baseline)
|
|
732
|
-
logger.info("Gmail poller baseline established",
|
|
733
|
-
node_id=node_id, count=len(seen_ids))
|
|
734
|
-
except Exception as e:
|
|
735
|
-
logger.warning("Gmail poller baseline failed",
|
|
736
|
-
node_id=node_id, error=str(e))
|
|
737
|
-
|
|
738
|
-
# Poll loop
|
|
739
|
-
poll_count = 0
|
|
740
|
-
while is_running_fn():
|
|
741
|
-
await asyncio.sleep(poll_interval)
|
|
742
|
-
if not is_running_fn():
|
|
743
|
-
break
|
|
744
|
-
|
|
745
|
-
poll_count += 1
|
|
746
|
-
try:
|
|
747
|
-
current_ids = await _poll_gmail_ids(service, query)
|
|
748
|
-
new_ids = current_ids - seen_ids
|
|
749
|
-
logger.debug("Gmail poll cycle",
|
|
750
|
-
node_id=node_id, cycle=poll_count,
|
|
751
|
-
current=len(current_ids),
|
|
752
|
-
seen=len(seen_ids),
|
|
753
|
-
new=len(new_ids))
|
|
754
|
-
|
|
755
|
-
for msg_id in new_ids:
|
|
756
|
-
seen_ids.add(msg_id)
|
|
757
|
-
email_data = await _fetch_email_details(service, msg_id)
|
|
758
|
-
|
|
759
|
-
if mark_as_read:
|
|
760
|
-
try:
|
|
761
|
-
await _mark_email_as_read(service, msg_id)
|
|
762
|
-
except Exception:
|
|
763
|
-
pass
|
|
764
|
-
|
|
765
|
-
await queue.put(email_data)
|
|
766
|
-
logger.info("Gmail poller: new email queued",
|
|
767
|
-
node_id=node_id,
|
|
768
|
-
subject=email_data.get('subject', ''))
|
|
769
|
-
|
|
770
|
-
except asyncio.CancelledError:
|
|
771
|
-
break
|
|
772
|
-
except Exception as e:
|
|
773
|
-
logger.error("Gmail poll error",
|
|
774
|
-
node_id=node_id, error=str(e))
|
|
775
|
-
|
|
776
|
-
return poll
|
|
777
|
-
|
|
778
|
-
def _create_email_poll_coroutine(self, node_id: str,
|
|
779
|
-
params: Dict[str, Any]) -> Callable:
|
|
780
|
-
"""Create Himalaya email polling coroutine for deployment mode."""
|
|
781
|
-
async def poll(queue: asyncio.Queue, is_running_fn: Callable):
|
|
782
|
-
from services.email_service import get_email_service
|
|
783
|
-
|
|
784
|
-
svc = get_email_service()
|
|
785
|
-
creds = await svc.resolve_credentials(params)
|
|
786
|
-
cfg = svc.resolve_poll_params(params)
|
|
787
|
-
|
|
788
|
-
seen = await svc.poll_ids(creds, cfg["folder"])
|
|
789
|
-
logger.info("Email poller starting",
|
|
790
|
-
node_id=node_id, folder=cfg["folder"], seen=len(seen))
|
|
791
|
-
|
|
792
|
-
while is_running_fn():
|
|
793
|
-
await asyncio.sleep(cfg["interval"])
|
|
794
|
-
if not is_running_fn():
|
|
795
|
-
break
|
|
796
|
-
try:
|
|
797
|
-
for msg_id in await svc.poll_ids(creds, cfg["folder"]) - seen:
|
|
798
|
-
seen.add(msg_id)
|
|
799
|
-
email_data = await svc.fetch_detail(creds, msg_id, cfg["folder"])
|
|
800
|
-
if cfg["mark_as_read"]:
|
|
801
|
-
try:
|
|
802
|
-
d = svc.defaults
|
|
803
|
-
await svc.himalaya.flag_message(
|
|
804
|
-
creds, msg_id, d.get("flag"), d.get("flag_action"), cfg["folder"])
|
|
805
|
-
except Exception:
|
|
806
|
-
pass
|
|
807
|
-
await queue.put(email_data)
|
|
808
|
-
except asyncio.CancelledError:
|
|
809
|
-
break
|
|
810
|
-
except Exception as e:
|
|
811
|
-
logger.error("Email poll error",
|
|
812
|
-
node_id=node_id, error=str(e))
|
|
813
|
-
|
|
814
|
-
return poll
|
|
690
|
+
# _create_poll_coroutine + _create_gmail_poll_coroutine +
|
|
691
|
+
# _create_email_poll_coroutine REMOVED in Wave 11.I, milestone L.
|
|
692
|
+
# Polling-coroutine factories now self-register from each plugin's
|
|
693
|
+
# PollingTriggerNode subclass (services.plugin.PollingTriggerNode)
|
|
694
|
+
# via services.deployment.poll_registry.register_poll_coroutine_factory.
|
|
695
|
+
# The dispatch path lives ~140 lines up in _setup_event_trigger.
|
|
815
696
|
|
|
816
697
|
async def _load_settings(self):
|
|
817
698
|
"""Load deployment settings from database."""
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Polling-coroutine factory registry (Wave 11.I, milestone L).
|
|
2
|
+
|
|
3
|
+
Plugin packages register a factory that produces an async polling
|
|
4
|
+
coroutine for a given trigger node type. ``DeploymentManager`` looks
|
|
5
|
+
up the factory at deploy time and hands the produced coroutine to
|
|
6
|
+
``TriggerManager.setup_polling_trigger`` -- the deployment manager no
|
|
7
|
+
longer hardcodes per-plugin polling switches.
|
|
8
|
+
|
|
9
|
+
Mirror of :func:`services.event_waiter.register_filter_builder`:
|
|
10
|
+
same idempotency contract, same audience (plugin ``__init__.py``
|
|
11
|
+
modules), built on the shared :class:`IdempotentRegistry`.
|
|
12
|
+
|
|
13
|
+
Factory signature
|
|
14
|
+
-----------------
|
|
15
|
+
|
|
16
|
+
factory(node_id: str, params: Dict[str, Any]) -> async (
|
|
17
|
+
queue: asyncio.Queue, is_running_fn: Callable[[], bool]
|
|
18
|
+
) -> None
|
|
19
|
+
|
|
20
|
+
The factory closes over node-specific config (auth, query, poll
|
|
21
|
+
interval); the returned coroutine drains until ``is_running_fn()``
|
|
22
|
+
returns False or the task is cancelled. New events go on ``queue``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import asyncio
|
|
28
|
+
from typing import Any, Awaitable, Callable, Dict, Optional
|
|
29
|
+
|
|
30
|
+
from services.plugin.registry import IdempotentRegistry
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# (queue, is_running) -> awaitable. The poll coroutine itself.
|
|
34
|
+
PollCoroutine = Callable[[asyncio.Queue, Callable[[], bool]], Awaitable[None]]
|
|
35
|
+
|
|
36
|
+
# (node_id, params) -> the bound poll coroutine.
|
|
37
|
+
PollCoroutineFactory = Callable[[str, Dict[str, Any]], PollCoroutine]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_REGISTRY: IdempotentRegistry[str, PollCoroutineFactory] = IdempotentRegistry(
|
|
41
|
+
"poll_coroutine_factory"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def register_poll_coroutine_factory(
|
|
46
|
+
node_type: str, factory: PollCoroutineFactory
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Publish a polling-coroutine factory for a trigger node type.
|
|
49
|
+
|
|
50
|
+
Idempotent on re-import (same callable for the same key is a
|
|
51
|
+
no-op). A different callable for an existing key raises
|
|
52
|
+
``ValueError`` to surface plugin namespace collisions early.
|
|
53
|
+
"""
|
|
54
|
+
_REGISTRY.register(node_type, factory)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_poll_coroutine_factory(node_type: str) -> Optional[PollCoroutineFactory]:
|
|
58
|
+
"""Return the factory for ``node_type``, or ``None`` if unregistered."""
|
|
59
|
+
return _REGISTRY.get(node_type)
|
|
@@ -85,16 +85,12 @@ class TriggerConfig:
|
|
|
85
85
|
# Registry of supported trigger types (event-based triggers only)
|
|
86
86
|
# Note: cronScheduler is NOT an event-based trigger - it uses APScheduler directly
|
|
87
87
|
TRIGGER_REGISTRY: Dict[str, TriggerConfig] = {
|
|
88
|
+
# Framework-level triggers — not owned by any plugin domain.
|
|
88
89
|
'start': TriggerConfig(
|
|
89
90
|
node_type='start',
|
|
90
91
|
event_type='deploy_triggered',
|
|
91
92
|
display_name='Deploy Start'
|
|
92
93
|
),
|
|
93
|
-
'whatsappReceive': TriggerConfig(
|
|
94
|
-
node_type='whatsappReceive',
|
|
95
|
-
event_type='whatsapp_message_received',
|
|
96
|
-
display_name='WhatsApp Message'
|
|
97
|
-
),
|
|
98
94
|
'webhookTrigger': TriggerConfig(
|
|
99
95
|
node_type='webhookTrigger',
|
|
100
96
|
event_type='webhook_received',
|
|
@@ -110,24 +106,14 @@ TRIGGER_REGISTRY: Dict[str, TriggerConfig] = {
|
|
|
110
106
|
event_type='task_completed',
|
|
111
107
|
display_name='Task Completed'
|
|
112
108
|
),
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
display_name='Gmail Email'
|
|
122
|
-
),
|
|
123
|
-
# 'telegramReceive' moved to nodes/telegram/ — backfilled here from
|
|
124
|
-
# the plugin's ``event_type`` class attribute via
|
|
125
|
-
# ``_auto_populate_from_plugins`` on first access.
|
|
126
|
-
'emailReceive': TriggerConfig(
|
|
127
|
-
node_type='emailReceive',
|
|
128
|
-
event_type='email_received',
|
|
129
|
-
display_name='Email'
|
|
130
|
-
),
|
|
109
|
+
# Plugin-owned trigger entries (whatsappReceive, twitterReceive,
|
|
110
|
+
# telegramReceive, emailReceive, googleGmailReceive) live in their
|
|
111
|
+
# plugin folders' ``_filters.py`` and are backfilled here from each
|
|
112
|
+
# plugin's ``event_type`` ClassVar via
|
|
113
|
+
# ``_auto_populate_from_plugins`` (Wave 11.I, milestone K). Gmail's
|
|
114
|
+
# explicit alias entry was retired in milestone P after the
|
|
115
|
+
# downstream callers (POLLING_TRIGGER_TYPES, deployment manager,
|
|
116
|
+
# frontend trigger list) were renamed to the canonical class type.
|
|
131
117
|
# Future triggers - just add to registry:
|
|
132
118
|
# 'mqttTrigger': TriggerConfig('mqttTrigger', 'mqtt_message', 'MQTT Message'),
|
|
133
119
|
}
|
|
@@ -213,126 +199,6 @@ def get_trigger_config(node_type: str) -> Optional[TriggerConfig]:
|
|
|
213
199
|
# FILTER BUILDERS - One per trigger type
|
|
214
200
|
# =============================================================================
|
|
215
201
|
|
|
216
|
-
def build_whatsapp_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
217
|
-
"""Build filter function for WhatsApp messages.
|
|
218
|
-
|
|
219
|
-
Based on Go RPC handleIncomingMessage() event fields (service.go):
|
|
220
|
-
- message_id: string - unique message ID
|
|
221
|
-
- sender: string - Sender JID (may be LID for groups)
|
|
222
|
-
- sender_phone: string - RESOLVED phone number (LID already resolved by Go RPC!)
|
|
223
|
-
- chat_id: string - Chat JID (same as sender for DMs, group JID for groups)
|
|
224
|
-
- timestamp: time - message timestamp
|
|
225
|
-
- is_from_me: boolean - true if sent by connected account
|
|
226
|
-
- is_group: boolean - true if message is in a group chat
|
|
227
|
-
- message_type: string - text, image, video, audio, document, sticker, location, contact, contacts
|
|
228
|
-
- text: string - text content (for text messages)
|
|
229
|
-
- is_forwarded: boolean - true if message is forwarded
|
|
230
|
-
- forwarding_score: int - forwarding count
|
|
231
|
-
- group_info: object - present for group messages:
|
|
232
|
-
- group_jid: string
|
|
233
|
-
- sender_jid: string
|
|
234
|
-
- sender_phone: string - RESOLVED phone number
|
|
235
|
-
- sender_name: string - push name if available
|
|
236
|
-
|
|
237
|
-
Note: The Go RPC already resolves LIDs to phone numbers before sending the event.
|
|
238
|
-
The sender_phone field contains the resolved phone number - no manual LID resolution needed!
|
|
239
|
-
"""
|
|
240
|
-
# Snake_case throughout — matches plugin Params (no camelCase aliases).
|
|
241
|
-
msg_type = params.get('message_type_filter', 'all')
|
|
242
|
-
sender_filter = params.get('filter', 'all')
|
|
243
|
-
contact_phone = params.get('phone_number', '')
|
|
244
|
-
group_id = params.get('group_id', '')
|
|
245
|
-
sender_number = params.get('sender_number', '')
|
|
246
|
-
keywords = [k.strip().lower() for k in params.get('keywords', '').split(',') if k.strip()]
|
|
247
|
-
ignore_own = params.get('ignore_own_messages', True)
|
|
248
|
-
forwarded_filter = params.get('forwarded_filter', 'all')
|
|
249
|
-
|
|
250
|
-
logger.debug(f"[WhatsAppFilter] Built: type={msg_type}, filter={sender_filter}, group_id='{group_id}', forwarded={forwarded_filter}")
|
|
251
|
-
|
|
252
|
-
def matches(m: Dict) -> bool:
|
|
253
|
-
msg_chat_id = m.get('chat_id', '')
|
|
254
|
-
is_group = m.get('is_group', False)
|
|
255
|
-
group_info = m.get('group_info', {})
|
|
256
|
-
|
|
257
|
-
# Use sender_phone directly - Go RPC already resolves LIDs to phone numbers!
|
|
258
|
-
# For group messages, prefer group_info.sender_phone, fall back to root sender_phone
|
|
259
|
-
if is_group:
|
|
260
|
-
sender_phone = group_info.get('sender_phone', '') or m.get('sender_phone', '')
|
|
261
|
-
else:
|
|
262
|
-
sender_phone = m.get('sender_phone', '')
|
|
263
|
-
|
|
264
|
-
# Fallback: extract phone from sender JID if sender_phone not available
|
|
265
|
-
if not sender_phone:
|
|
266
|
-
sender = m.get('sender', '')
|
|
267
|
-
sender_phone = sender.split('@')[0] if '@' in sender else sender
|
|
268
|
-
|
|
269
|
-
# Message type filter (schema field: message_type)
|
|
270
|
-
if msg_type != 'all' and m.get('message_type') != msg_type:
|
|
271
|
-
return False
|
|
272
|
-
|
|
273
|
-
# Sender filter - for contact filter, use actual phone number
|
|
274
|
-
if sender_filter == 'self':
|
|
275
|
-
# Only accept messages in self-chat (notes to self)
|
|
276
|
-
# Must be from me AND in a chat with myself (not replies to others)
|
|
277
|
-
if not m.get('is_from_me'):
|
|
278
|
-
return False
|
|
279
|
-
# Check chat_id matches sender (self-chat)
|
|
280
|
-
chat_id = m.get('chat_id', '')
|
|
281
|
-
sender = m.get('sender', '')
|
|
282
|
-
if chat_id != sender:
|
|
283
|
-
return False
|
|
284
|
-
|
|
285
|
-
if sender_filter == 'any_contact':
|
|
286
|
-
# Only accept non-group messages (individual/contact messages)
|
|
287
|
-
if is_group:
|
|
288
|
-
return False
|
|
289
|
-
|
|
290
|
-
if sender_filter == 'contact':
|
|
291
|
-
if contact_phone not in sender_phone:
|
|
292
|
-
return False
|
|
293
|
-
|
|
294
|
-
if sender_filter == 'group':
|
|
295
|
-
# For group filter, check if message is from that group
|
|
296
|
-
if not is_group:
|
|
297
|
-
return False
|
|
298
|
-
if msg_chat_id != group_id:
|
|
299
|
-
return False
|
|
300
|
-
# Optional: filter by specific sender within group using resolved phone number
|
|
301
|
-
if sender_number:
|
|
302
|
-
if sender_number not in sender_phone:
|
|
303
|
-
return False
|
|
304
|
-
|
|
305
|
-
if sender_filter == 'channel':
|
|
306
|
-
# Only accept messages from newsletter channels (chat_id ends with @newsletter)
|
|
307
|
-
if not msg_chat_id.endswith('@newsletter'):
|
|
308
|
-
return False
|
|
309
|
-
# If specific channel JID provided, match exactly
|
|
310
|
-
channel_jid = params.get('channel_jid', '')
|
|
311
|
-
if channel_jid and msg_chat_id != channel_jid:
|
|
312
|
-
return False
|
|
313
|
-
|
|
314
|
-
if sender_filter == 'keywords':
|
|
315
|
-
text = (m.get('text') or '').lower()
|
|
316
|
-
if not any(kw in text for kw in keywords):
|
|
317
|
-
return False
|
|
318
|
-
|
|
319
|
-
# Ignore own messages (schema field: is_from_me) - but not when filtering for 'self'
|
|
320
|
-
if ignore_own and sender_filter != 'self' and m.get('is_from_me'):
|
|
321
|
-
return False
|
|
322
|
-
|
|
323
|
-
# Forwarded message filter (schema field: is_forwarded)
|
|
324
|
-
is_forwarded = m.get('is_forwarded', False)
|
|
325
|
-
if forwarded_filter == 'only_forwarded' and not is_forwarded:
|
|
326
|
-
return False
|
|
327
|
-
if forwarded_filter == 'ignore_forwarded' and is_forwarded:
|
|
328
|
-
return False
|
|
329
|
-
|
|
330
|
-
logger.debug(f"[WhatsAppFilter] Matched message from {sender_phone}")
|
|
331
|
-
return True
|
|
332
|
-
|
|
333
|
-
return matches
|
|
334
|
-
|
|
335
|
-
|
|
336
202
|
def build_webhook_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
337
203
|
"""Build filter function for webhook requests.
|
|
338
204
|
|
|
@@ -425,96 +291,29 @@ def build_task_completed_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
|
425
291
|
return matches
|
|
426
292
|
|
|
427
293
|
|
|
428
|
-
def build_twitter_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
429
|
-
"""Build filter function for Twitter events.
|
|
430
|
-
|
|
431
|
-
Filters by:
|
|
432
|
-
- trigger_type: 'mentions', 'search', 'user_timeline'
|
|
433
|
-
- search_query: Search query for 'search' trigger type
|
|
434
|
-
- user_id: User ID for 'user_timeline' trigger type
|
|
435
|
-
|
|
436
|
-
Args:
|
|
437
|
-
params: Node parameters
|
|
438
|
-
|
|
439
|
-
Returns:
|
|
440
|
-
Filter function that checks if event matches criteria
|
|
441
|
-
"""
|
|
442
|
-
trigger_type = params.get('trigger_type', 'mentions')
|
|
443
|
-
search_query = params.get('search_query', '')
|
|
444
|
-
user_id = params.get('user_id', '')
|
|
445
|
-
|
|
446
|
-
def matches(data: Dict) -> bool:
|
|
447
|
-
event_type = data.get('trigger_type', '')
|
|
448
|
-
if trigger_type != 'all' and event_type != trigger_type:
|
|
449
|
-
return False
|
|
450
|
-
if trigger_type == 'search' and search_query:
|
|
451
|
-
# Check if search query matches
|
|
452
|
-
event_query = data.get('query', '')
|
|
453
|
-
if search_query.lower() not in event_query.lower():
|
|
454
|
-
return False
|
|
455
|
-
if trigger_type == 'user_timeline' and user_id:
|
|
456
|
-
if data.get('user_id') != user_id:
|
|
457
|
-
return False
|
|
458
|
-
return True
|
|
459
|
-
|
|
460
|
-
return matches
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
def build_gmail_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
464
|
-
"""Build filter function for Gmail email events.
|
|
465
|
-
|
|
466
|
-
Filters by label to ensure the event matches the trigger's label filter.
|
|
467
|
-
The filter_query is applied at the Gmail API level during polling,
|
|
468
|
-
so this filter only checks labels for events dispatched via event_waiter.
|
|
469
|
-
|
|
470
|
-
Args:
|
|
471
|
-
params: Node parameters with 'label_filter' field
|
|
472
|
-
|
|
473
|
-
Returns:
|
|
474
|
-
Filter function that checks if event labels match
|
|
475
|
-
"""
|
|
476
|
-
label_filter = params.get('label_filter', 'INBOX')
|
|
477
|
-
|
|
478
|
-
def matches(data: Dict) -> bool:
|
|
479
|
-
if label_filter and label_filter != 'all':
|
|
480
|
-
labels = data.get('labels', [])
|
|
481
|
-
if label_filter not in labels:
|
|
482
|
-
return False
|
|
483
|
-
return True
|
|
484
|
-
|
|
485
|
-
return matches
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
def build_email_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
489
|
-
"""Build filter for email events (Himalaya IMAP polling)."""
|
|
490
|
-
folder_filter = params.get('folder', 'INBOX')
|
|
491
|
-
filter_query = params.get('filter_query', '')
|
|
492
|
-
|
|
493
|
-
def matches(data: Dict) -> bool:
|
|
494
|
-
if folder_filter and folder_filter != 'all':
|
|
495
|
-
if data.get('folder', '') != folder_filter:
|
|
496
|
-
return False
|
|
497
|
-
return True
|
|
498
|
-
|
|
499
|
-
return matches
|
|
500
|
-
|
|
501
|
-
|
|
502
294
|
# Registry of filter builders per trigger type. Plugin packages
|
|
503
295
|
# (nodes/<group>/) call :func:`register_filter_builder` from their
|
|
504
296
|
# package ``__init__.py`` to publish per-trigger filters without this
|
|
505
|
-
# module needing to import them. The hardcoded entries below are
|
|
506
|
-
# triggers
|
|
507
|
-
#
|
|
297
|
+
# module needing to import them. The hardcoded entries below are
|
|
298
|
+
# **framework-level** triggers (webhook + chat + delegated-task) --
|
|
299
|
+
# they don't belong to any one plugin domain and intentionally stay
|
|
300
|
+
# here. The plugin entries (whatsappReceive, twitterReceive,
|
|
301
|
+
# googleGmailReceive, emailReceive) live in their plugin folders'
|
|
302
|
+
# ``_filters.py`` and self-register at import time.
|
|
508
303
|
FILTER_BUILDERS: Dict[str, Callable[[Dict], Callable[[Dict], bool]]] = {
|
|
509
|
-
'whatsappReceive': build_whatsapp_filter,
|
|
510
304
|
'webhookTrigger': build_webhook_filter,
|
|
511
305
|
'chatTrigger': build_chat_filter,
|
|
512
306
|
'taskTrigger': build_task_completed_filter,
|
|
513
|
-
'twitterReceive': build_twitter_filter,
|
|
514
|
-
'gmailReceive': build_gmail_filter,
|
|
515
|
-
'emailReceive': build_email_filter,
|
|
516
307
|
}
|
|
517
308
|
|
|
309
|
+
from services.plugin.registry import IdempotentRegistry as _IdempotentRegistry # noqa: E402
|
|
310
|
+
|
|
311
|
+
# Backed by the module-level FILTER_BUILDERS dict so existing readers
|
|
312
|
+
# (e.g. build_filter, _ensure_populated, tests) keep working.
|
|
313
|
+
_FILTER_REGISTRY: _IdempotentRegistry[str, Callable[[Dict], Callable[[Dict], bool]]] = (
|
|
314
|
+
_IdempotentRegistry("filter_builder", items=FILTER_BUILDERS)
|
|
315
|
+
)
|
|
316
|
+
|
|
518
317
|
|
|
519
318
|
def register_filter_builder(
|
|
520
319
|
node_type: str,
|
|
@@ -526,13 +325,7 @@ def register_filter_builder(
|
|
|
526
325
|
Used by plugin packages to keep all per-node-type knowledge inside
|
|
527
326
|
the plugin folder instead of hardcoding it here.
|
|
528
327
|
"""
|
|
529
|
-
|
|
530
|
-
if existing is not None and existing is not builder:
|
|
531
|
-
raise ValueError(
|
|
532
|
-
f"Filter builder for '{node_type}' is already registered by "
|
|
533
|
-
f"{existing.__module__}.{existing.__qualname__}"
|
|
534
|
-
)
|
|
535
|
-
FILTER_BUILDERS[node_type] = builder
|
|
328
|
+
_FILTER_REGISTRY.register(node_type, builder)
|
|
536
329
|
|
|
537
330
|
|
|
538
331
|
def build_filter(node_type: str, params: Dict) -> Callable[[Dict], bool]:
|
|
@@ -568,6 +361,9 @@ import inspect as _inspect
|
|
|
568
361
|
|
|
569
362
|
_TriggerPrecheck = Callable[[Dict[str, Any]], Any]
|
|
570
363
|
_TRIGGER_PRECHECKS: Dict[str, _TriggerPrecheck] = {}
|
|
364
|
+
_TRIGGER_PRECHECK_REGISTRY: _IdempotentRegistry[str, _TriggerPrecheck] = (
|
|
365
|
+
_IdempotentRegistry("trigger_precheck", items=_TRIGGER_PRECHECKS)
|
|
366
|
+
)
|
|
571
367
|
|
|
572
368
|
|
|
573
369
|
def register_trigger_precheck(node_type: str, fn: _TriggerPrecheck) -> None:
|
|
@@ -576,13 +372,7 @@ def register_trigger_precheck(node_type: str, fn: _TriggerPrecheck) -> None:
|
|
|
576
372
|
Idempotent on re-import. The callback may be sync or async; ``run_trigger_precheck``
|
|
577
373
|
awaits the coroutine when needed.
|
|
578
374
|
"""
|
|
579
|
-
|
|
580
|
-
if existing is not None and existing is not fn:
|
|
581
|
-
raise ValueError(
|
|
582
|
-
f"Trigger precheck for '{node_type}' is already registered by "
|
|
583
|
-
f"{existing.__module__}.{existing.__qualname__}"
|
|
584
|
-
)
|
|
585
|
-
_TRIGGER_PRECHECKS[node_type] = fn
|
|
375
|
+
_TRIGGER_PRECHECK_REGISTRY.register(node_type, fn)
|
|
586
376
|
|
|
587
377
|
|
|
588
378
|
async def run_trigger_precheck(node_type: str, parameters: Dict) -> Any:
|
|
@@ -826,43 +616,83 @@ def _cleanup_waiter(waiter_id: str) -> None:
|
|
|
826
616
|
# EVENT DISPATCH
|
|
827
617
|
# =============================================================================
|
|
828
618
|
|
|
829
|
-
|
|
619
|
+
def _unpack_event(
|
|
620
|
+
event: "Any",
|
|
621
|
+
data: Optional[Dict] = None,
|
|
622
|
+
) -> tuple[str, Dict]:
|
|
623
|
+
"""Normalise either ``(WorkflowEvent,)`` or ``(event_type, data)`` to
|
|
624
|
+
the underlying ``(event_type, data)`` pair the dispatcher uses.
|
|
625
|
+
|
|
626
|
+
Wave 11.I, milestone Q: ``dispatch`` and ``dispatch_async`` accept a
|
|
627
|
+
``WorkflowEvent`` directly so the ``WorkflowEvent(**event)`` rewrap
|
|
628
|
+
in ``events/triggers.py`` becomes a no-op. The legacy
|
|
629
|
+
``(event_type, data)`` shape stays supported via
|
|
630
|
+
:meth:`WorkflowEvent.from_legacy` upstream of the dispatcher (the
|
|
631
|
+
public API just forwards either form).
|
|
632
|
+
"""
|
|
633
|
+
# Lazy import: services.events imports event_waiter for trigger
|
|
634
|
+
# adaptation, so a top-level import would be circular.
|
|
635
|
+
from services.events.envelope import WorkflowEvent
|
|
636
|
+
|
|
637
|
+
if isinstance(event, WorkflowEvent):
|
|
638
|
+
return event.type, event.data if isinstance(event.data, dict) else {"data": event.data}
|
|
639
|
+
if isinstance(event, str):
|
|
640
|
+
return event, data or {}
|
|
641
|
+
raise TypeError(
|
|
642
|
+
f"dispatch expects a WorkflowEvent or (event_type: str, data: Dict); "
|
|
643
|
+
f"got {type(event).__name__}"
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
async def dispatch_async(
|
|
648
|
+
event: "Any",
|
|
649
|
+
data: Optional[Dict] = None,
|
|
650
|
+
) -> int:
|
|
830
651
|
"""Dispatch event asynchronously (for Redis mode).
|
|
831
652
|
|
|
832
653
|
Args:
|
|
833
|
-
|
|
834
|
-
|
|
654
|
+
event: Either a :class:`WorkflowEvent` (preferred, Wave 11.I) or
|
|
655
|
+
an event-type string. The legacy ``(event_type, data)`` form
|
|
656
|
+
stays supported.
|
|
657
|
+
data: Event payload (when ``event`` is a string).
|
|
835
658
|
|
|
836
659
|
Returns:
|
|
837
660
|
1 if event was added to stream, 0 otherwise
|
|
838
661
|
"""
|
|
662
|
+
event_type, payload = _unpack_event(event, data)
|
|
839
663
|
logger.debug(f"[EventWaiter] dispatch_async: event_type='{event_type}'")
|
|
840
664
|
|
|
841
665
|
if is_redis_mode():
|
|
842
666
|
cache = get_cache_service()
|
|
843
667
|
stream_name = _get_stream_name(event_type)
|
|
844
|
-
msg_id = await cache.stream_add(stream_name,
|
|
668
|
+
msg_id = await cache.stream_add(stream_name, payload)
|
|
845
669
|
if msg_id:
|
|
846
670
|
logger.debug(f"[EventWaiter] Added event to stream {stream_name}: {msg_id}")
|
|
847
671
|
return 1
|
|
848
672
|
return 0
|
|
849
673
|
else:
|
|
850
674
|
# Fall back to sync dispatch for memory mode
|
|
851
|
-
return dispatch(event_type,
|
|
675
|
+
return dispatch(event_type, payload)
|
|
852
676
|
|
|
853
677
|
|
|
854
|
-
def dispatch(
|
|
678
|
+
def dispatch(
|
|
679
|
+
event: "Any",
|
|
680
|
+
data: Optional[Dict] = None,
|
|
681
|
+
) -> int:
|
|
855
682
|
"""Dispatch event to matching waiters (synchronous, memory mode).
|
|
856
683
|
|
|
857
684
|
Thread-safe: Can be called from APScheduler threads or async context.
|
|
858
685
|
|
|
859
686
|
Args:
|
|
860
|
-
|
|
861
|
-
|
|
687
|
+
event: Either a :class:`WorkflowEvent` (preferred, Wave 11.I) or
|
|
688
|
+
an event-type string. The legacy ``(event_type, data)`` form
|
|
689
|
+
stays supported.
|
|
690
|
+
data: Event payload (when ``event`` is a string).
|
|
862
691
|
|
|
863
692
|
Returns:
|
|
864
693
|
Number of waiters resolved
|
|
865
694
|
"""
|
|
695
|
+
event_type, data = _unpack_event(event, data)
|
|
866
696
|
if is_redis_mode():
|
|
867
697
|
# In Redis mode, use async dispatch
|
|
868
698
|
# Handle both async context and thread context (e.g., APScheduler callbacks)
|