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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Google Workspace service-status refresh callback (Wave 11.I, milestone J).
|
|
2
|
+
|
|
3
|
+
Moved from ``services/status_broadcaster._refresh_google_status``.
|
|
4
|
+
Plugin packages register their own callback via
|
|
5
|
+
``status_broadcaster.register_service_refresh``; the broadcaster no
|
|
6
|
+
longer hardcodes a per-service refresh.
|
|
7
|
+
|
|
8
|
+
Reads OAuth tokens via ``auth_service.get_oauth_tokens("google")`` and
|
|
9
|
+
mirrors the result into the broadcaster cache.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from opentelemetry import trace
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from services.status_broadcaster import StatusBroadcaster
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
tracer = trace.get_tracer(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def refresh_google_status(broadcaster: "StatusBroadcaster") -> None:
|
|
27
|
+
"""Refresh Google cache + broadcast. One pass per
|
|
28
|
+
``_refresh_all_services`` cycle.
|
|
29
|
+
"""
|
|
30
|
+
with tracer.start_as_current_span("broadcaster.refresh_google") as span:
|
|
31
|
+
try:
|
|
32
|
+
from services.plugin.deps import get_auth_service
|
|
33
|
+
|
|
34
|
+
auth_service = get_auth_service()
|
|
35
|
+
tokens = await auth_service.get_oauth_tokens(
|
|
36
|
+
"google", customer_id="owner"
|
|
37
|
+
)
|
|
38
|
+
if not tokens or not tokens.get("access_token"):
|
|
39
|
+
broadcaster._status["google"] = {
|
|
40
|
+
"connected": False,
|
|
41
|
+
"email": None,
|
|
42
|
+
"name": None,
|
|
43
|
+
}
|
|
44
|
+
else:
|
|
45
|
+
broadcaster._status["google"] = {
|
|
46
|
+
"connected": True,
|
|
47
|
+
"email": tokens.get("email"),
|
|
48
|
+
"name": tokens.get("name"),
|
|
49
|
+
}
|
|
50
|
+
logger.debug(
|
|
51
|
+
"[StatusBroadcaster] Google status: connected as %s",
|
|
52
|
+
tokens.get("email"),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
await broadcaster.broadcast({
|
|
56
|
+
"type": "google_status",
|
|
57
|
+
"data": broadcaster._status["google"],
|
|
58
|
+
})
|
|
59
|
+
span.set_attribute(
|
|
60
|
+
"connected", bool(broadcaster._status["google"]["connected"])
|
|
61
|
+
)
|
|
62
|
+
except Exception as exc: # noqa: BLE001
|
|
63
|
+
span.record_exception(exc)
|
|
64
|
+
logger.debug(
|
|
65
|
+
"[StatusBroadcaster] Could not refresh Google status: %s", exc
|
|
66
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Google Workspace OAuth callback router — factory-built (Wave 11.I, S.2).
|
|
2
|
+
|
|
3
|
+
Two endpoints:
|
|
4
|
+
|
|
5
|
+
* ``GET /api/google/callback`` -- built by
|
|
6
|
+
:func:`services.events.oauth_lifecycle.make_oauth_callback_router`.
|
|
7
|
+
``extra_state_handler`` routes customer-mode logins (where
|
|
8
|
+
``state_data["mode"] == "customer"`` and a ``customer_id`` is set)
|
|
9
|
+
to the ``google_connections`` table by overriding ``customer_id``
|
|
10
|
+
on ``store_oauth_tokens``. ``redirect_after`` from the same state
|
|
11
|
+
data triggers a 302 to the customer-portal URL.
|
|
12
|
+
* ``POST /api/google/customer-auth-url`` -- generates an OAuth URL for
|
|
13
|
+
a specific customer (Google-only multi-tenant feature; Twitter
|
|
14
|
+
ships owner-mode only).
|
|
15
|
+
|
|
16
|
+
The pre-S file also exposed ``GET /status`` and ``POST /logout`` REST
|
|
17
|
+
routes; both duplicated the WS handlers in ``_handlers.py`` and have
|
|
18
|
+
been retired in line with the Twitter migration.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any, Dict, Optional
|
|
24
|
+
|
|
25
|
+
from fastapi import APIRouter, Request
|
|
26
|
+
|
|
27
|
+
from services.events.oauth_lifecycle import make_oauth_callback_router
|
|
28
|
+
|
|
29
|
+
from ._handlers import _google_oauth_factory
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _user_info_to_email(info: Dict[str, Any]) -> str:
|
|
33
|
+
return info.get("email", "Unknown") or "Unknown"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _user_info_to_name(info: Dict[str, Any]) -> str:
|
|
37
|
+
return info.get("name", "") or ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def _customer_mode_handler(
|
|
41
|
+
payload: Dict[str, Any],
|
|
42
|
+
) -> Optional[Dict[str, Any]]:
|
|
43
|
+
"""If the auth flow was launched in customer mode, route the
|
|
44
|
+
token storage to the customer's slot + return a 302 target.
|
|
45
|
+
|
|
46
|
+
Owner mode -> returns ``None`` (lifecycle factory keeps its
|
|
47
|
+
defaults).
|
|
48
|
+
"""
|
|
49
|
+
state_data = payload.get("state_data") or {}
|
|
50
|
+
if state_data.get("mode") != "customer":
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
customer_id = state_data.get("customer_id")
|
|
54
|
+
if not customer_id:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
user_info = payload.get("user_info") or {}
|
|
58
|
+
email = user_info.get("email", "Unknown")
|
|
59
|
+
redirect_after = state_data.get("redirect_after")
|
|
60
|
+
|
|
61
|
+
overrides: Dict[str, Any] = {"customer_id": customer_id}
|
|
62
|
+
if redirect_after:
|
|
63
|
+
overrides["redirect_after"] = (
|
|
64
|
+
f"{redirect_after}?google_connected=true"
|
|
65
|
+
f"&customer={customer_id}&email={email}"
|
|
66
|
+
)
|
|
67
|
+
return overrides
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# The factory mounts ``GET /api/google/callback``. Google-specific
|
|
71
|
+
# extras (the customer-auth-url route + the customer-mode hook) layer
|
|
72
|
+
# on top of the same router instance.
|
|
73
|
+
router: APIRouter = make_oauth_callback_router(
|
|
74
|
+
provider="google",
|
|
75
|
+
oauth_factory=_google_oauth_factory,
|
|
76
|
+
user_info_to_email=_user_info_to_email,
|
|
77
|
+
user_info_to_name=_user_info_to_name,
|
|
78
|
+
extra_state_handler=_customer_mode_handler,
|
|
79
|
+
color_hex="#34a853",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.post("/customer-auth-url")
|
|
84
|
+
async def generate_customer_auth_url(
|
|
85
|
+
request: Request, customer_id: str, redirect_after: Optional[str] = None,
|
|
86
|
+
):
|
|
87
|
+
"""Generate OAuth URL for a customer to connect their Google account."""
|
|
88
|
+
from services.oauth_utils import get_redirect_uri
|
|
89
|
+
|
|
90
|
+
redirect_uri = get_redirect_uri(request, "google")
|
|
91
|
+
oauth = await _google_oauth_factory(redirect_uri=redirect_uri)
|
|
92
|
+
if not oauth.client_id or not oauth.client_secret:
|
|
93
|
+
return {
|
|
94
|
+
"success": False,
|
|
95
|
+
"error": "Google not configured. Add Client ID and Secret.",
|
|
96
|
+
}
|
|
97
|
+
result = oauth.generate_authorization_url(
|
|
98
|
+
state_data={
|
|
99
|
+
"customer_id": customer_id,
|
|
100
|
+
"redirect_after": redirect_after,
|
|
101
|
+
"mode": "customer",
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
return {"success": True, "url": result["url"], "state": result["state"]}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.get("/customer/{customer_id}/status")
|
|
108
|
+
async def get_customer_google_status(customer_id: str):
|
|
109
|
+
"""Get Google connection status for a customer."""
|
|
110
|
+
from services.plugin.deps import get_auth_service
|
|
111
|
+
|
|
112
|
+
auth_service = get_auth_service()
|
|
113
|
+
tokens = await auth_service.get_oauth_tokens("google", customer_id=customer_id)
|
|
114
|
+
if not tokens:
|
|
115
|
+
return {"connected": False, "customer_id": customer_id}
|
|
116
|
+
return {
|
|
117
|
+
"connected": True,
|
|
118
|
+
"customer_id": customer_id,
|
|
119
|
+
"email": tokens.get("email"),
|
|
120
|
+
"name": tokens.get("name"),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@router.post("/customer/{customer_id}/disconnect")
|
|
125
|
+
async def disconnect_customer_google(customer_id: str):
|
|
126
|
+
"""Disconnect a customer's Google account."""
|
|
127
|
+
from services.plugin.deps import get_auth_service
|
|
128
|
+
|
|
129
|
+
auth_service = get_auth_service()
|
|
130
|
+
await auth_service.remove_oauth_tokens("google", customer_id=customer_id)
|
|
131
|
+
return {"success": True, "customer_id": customer_id}
|
|
@@ -10,7 +10,9 @@ from typing import Any, Dict, Literal, Optional, Set
|
|
|
10
10
|
from pydantic import BaseModel, ConfigDict, Field
|
|
11
11
|
|
|
12
12
|
from core.logging import get_logger
|
|
13
|
-
from services.plugin import
|
|
13
|
+
from services.plugin import (
|
|
14
|
+
NodeContext, Operation, PollingTriggerNode, TaskQueue,
|
|
15
|
+
)
|
|
14
16
|
|
|
15
17
|
from ._credentials import GoogleCredential
|
|
16
18
|
|
|
@@ -52,8 +54,16 @@ class GmailReceiveOutput(BaseModel):
|
|
|
52
54
|
model_config = ConfigDict(extra="allow")
|
|
53
55
|
|
|
54
56
|
|
|
55
|
-
class GmailReceiveNode(
|
|
57
|
+
class GmailReceiveNode(PollingTriggerNode):
|
|
56
58
|
type = "googleGmailReceive"
|
|
59
|
+
# Wave 11.I, milestone P: ``event_type`` ClassVar lets
|
|
60
|
+
# ``event_waiter._auto_populate_from_plugins`` backfill
|
|
61
|
+
# TRIGGER_REGISTRY without a hardcoded entry in event_waiter. The
|
|
62
|
+
# legacy ``type_alias = "gmailReceive"`` shim was retired -- the
|
|
63
|
+
# downstream callers (POLLING_TRIGGER_TYPES, deployment manager,
|
|
64
|
+
# frontend trigger list, _filters registration) all rename to the
|
|
65
|
+
# canonical class type in the same commit.
|
|
66
|
+
event_type = "gmail_email_received"
|
|
57
67
|
display_name = "Gmail Receive"
|
|
58
68
|
subtitle = "Inbound Email"
|
|
59
69
|
group = ("google", "trigger")
|
|
@@ -65,12 +75,39 @@ class GmailReceiveNode(TriggerNode):
|
|
|
65
75
|
)
|
|
66
76
|
credentials = (GoogleCredential,)
|
|
67
77
|
task_queue = TaskQueue.TRIGGERS_POLL
|
|
68
|
-
mode = "polling"
|
|
69
78
|
default_poll_interval = 60
|
|
70
79
|
|
|
71
80
|
Params = GmailReceiveParams
|
|
72
81
|
Output = GmailReceiveOutput
|
|
73
82
|
|
|
83
|
+
# ---- PollingTriggerNode hooks (deployment-mode loop) -------------
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _build_query(parameters: Dict[str, Any]) -> str:
|
|
87
|
+
query = parameters.get("filter_query", "is:unread")
|
|
88
|
+
label = parameters.get("label_filter", "INBOX")
|
|
89
|
+
return f"label:{label} {query}" if label and label != "all" else query
|
|
90
|
+
|
|
91
|
+
async def setup_service(self, params: Dict[str, Any]) -> Any:
|
|
92
|
+
return await build_google_service("gmail", "v1", params, {})
|
|
93
|
+
|
|
94
|
+
async def fetch_ids(self, service: Any, params: Dict[str, Any]) -> Set[str]:
|
|
95
|
+
return await poll_gmail_ids(service, self._build_query(params))
|
|
96
|
+
|
|
97
|
+
async def fetch_detail(
|
|
98
|
+
self, service: Any, msg_id: str, params: Dict[str, Any]
|
|
99
|
+
) -> Dict[str, Any]:
|
|
100
|
+
return await fetch_email_details(service, msg_id)
|
|
101
|
+
|
|
102
|
+
async def post_emit(
|
|
103
|
+
self, service: Any, msg_id: str, params: Dict[str, Any]
|
|
104
|
+
) -> None:
|
|
105
|
+
if params.get("mark_as_read"):
|
|
106
|
+
try:
|
|
107
|
+
await mark_email_as_read(service, msg_id)
|
|
108
|
+
except Exception as exc: # noqa: BLE001
|
|
109
|
+
logger.warning(f"[GmailReceive] Failed to mark as read: {exc}")
|
|
110
|
+
|
|
74
111
|
async def execute(
|
|
75
112
|
self,
|
|
76
113
|
node_id: str,
|
|
@@ -168,5 +205,5 @@ class GmailReceiveNode(TriggerNode):
|
|
|
168
205
|
@Operation("wait")
|
|
169
206
|
async def wait(self, ctx: NodeContext, params: GmailReceiveParams) -> GmailReceiveOutput:
|
|
170
207
|
raise NotImplementedError(
|
|
171
|
-
"
|
|
208
|
+
"googleGmailReceive uses execute() override (Wave 11.D.5 inlined)."
|
|
172
209
|
)
|
package/server/nodes/groups.py
CHANGED
|
@@ -49,6 +49,7 @@ register_group(key="scheduler", metadata={"label": "Schedulers", "icon"
|
|
|
49
49
|
register_group(key="proxy", metadata={"label": "Proxy", "icon": "🛡", "color": "#bd93f9", "visibility": "dev"})
|
|
50
50
|
register_group(key="whatsapp", metadata={"label": "WhatsApp", "icon": "💬", "color": "#25D366", "visibility": "dev"})
|
|
51
51
|
register_group(key="email", metadata={"label": "Email", "icon": "✉️", "color": "#8be9fd", "visibility": "dev"})
|
|
52
|
+
register_group(key="payments", metadata={"label": "Payments", "icon": "asset:stripe", "color": "#635BFF", "visibility": "dev"})
|
|
52
53
|
register_group(key="browser", metadata={"label": "Browser", "icon": "🌐", "color": "#ff79c6", "visibility": "dev"})
|
|
53
54
|
register_group(key="scraper", metadata={"label": "Scrapers", "icon": "🕸", "color": "#ff79c6", "visibility": "dev"})
|
|
54
55
|
register_group(key="filesystem", metadata={"label": "Filesystem", "icon": "📁", "color": "#8be9fd", "visibility": "dev"})
|
|
@@ -8,7 +8,17 @@ gmaps_locations, gmaps_nearby_places). Separate from
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from services.plugin.credential import ApiKeyCredential, ProbeResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Google's documented sentinel address for connectivity tests on the
|
|
17
|
+
# Geocoding API. Returns ``status=OK`` for any working key with the
|
|
18
|
+
# Geocoding API enabled; ``REQUEST_DENIED`` for a bad key or disabled
|
|
19
|
+
# API. We never store the response — it's just a probe.
|
|
20
|
+
_GEOCODE_SENTINEL = "1600 Amphitheatre Parkway, Mountain View, CA"
|
|
21
|
+
_GEOCODE_URL = "https://maps.googleapis.com/maps/api/geocode/json"
|
|
12
22
|
|
|
13
23
|
|
|
14
24
|
class GoogleMapsCredential(ApiKeyCredential):
|
|
@@ -19,3 +29,37 @@ class GoogleMapsCredential(ApiKeyCredential):
|
|
|
19
29
|
key_name = "key"
|
|
20
30
|
key_location = "query"
|
|
21
31
|
docs_url = "https://developers.google.com/maps/documentation"
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
async def _probe(cls, api_key: str) -> ProbeResult:
|
|
35
|
+
"""Geocode a sentinel address to verify the key is live.
|
|
36
|
+
|
|
37
|
+
Replaces ``handle_validate_maps_key`` in
|
|
38
|
+
``routers/websocket.py``; the storage + broadcast wiring is
|
|
39
|
+
handled by the base :meth:`Credential.validate` so this method
|
|
40
|
+
only needs to translate the geocode response into a
|
|
41
|
+
:class:`ProbeResult`. ``REQUEST_DENIED`` → invalid; ``OK`` /
|
|
42
|
+
``ZERO_RESULTS`` / ``OVER_QUERY_LIMIT`` → valid (the key
|
|
43
|
+
authenticated, even if the query didn't return rows).
|
|
44
|
+
"""
|
|
45
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
46
|
+
response = await client.get(
|
|
47
|
+
_GEOCODE_URL,
|
|
48
|
+
params={"address": _GEOCODE_SENTINEL, "key": api_key},
|
|
49
|
+
)
|
|
50
|
+
payload = response.json()
|
|
51
|
+
status = payload.get("status")
|
|
52
|
+
|
|
53
|
+
if status == "REQUEST_DENIED":
|
|
54
|
+
return ProbeResult(
|
|
55
|
+
valid=False,
|
|
56
|
+
message=payload.get("error_message", "Invalid API key"),
|
|
57
|
+
)
|
|
58
|
+
return ProbeResult(
|
|
59
|
+
valid=True,
|
|
60
|
+
message=(
|
|
61
|
+
"Google Maps API key is valid"
|
|
62
|
+
if status == "OK"
|
|
63
|
+
else f"API key is valid (status: {status})"
|
|
64
|
+
),
|
|
65
|
+
)
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
"""Google Maps service for location operations.
|
|
1
|
+
"""Google Maps service for location operations.
|
|
2
|
+
|
|
3
|
+
Wave 11.I, milestone N: relocated from ``services/maps.py`` into the
|
|
4
|
+
location plugin folder. Three nodes consume it through the DI container
|
|
5
|
+
(``container.maps_service()``): ``gmaps_create``, ``gmaps_locations``,
|
|
6
|
+
``gmaps_nearby_places``. The container provider in ``core/container.py``
|
|
7
|
+
imports ``MapsService`` from this module.
|
|
8
|
+
|
|
9
|
+
API-key validation lives in :mod:`._credentials`'s
|
|
10
|
+
:class:`GoogleMapsCredential._probe` (Wave 11.I scaffold) -- the legacy
|
|
11
|
+
REST endpoint at ``/python/maps/validate-key`` was retired with this
|
|
12
|
+
move. Workflow nodes still call ``MapsService.create_map`` /
|
|
13
|
+
``geocode_location`` / ``find_nearby_places`` directly via the DI
|
|
14
|
+
container; the dead ``/python/<name>/execute`` REST endpoints (also
|
|
15
|
+
retired) duplicated the same paths.
|
|
16
|
+
"""
|
|
2
17
|
|
|
3
18
|
import time
|
|
4
19
|
import googlemaps
|
|
@@ -32,13 +47,13 @@ async def _track_maps_usage(
|
|
|
32
47
|
Returns:
|
|
33
48
|
Cost breakdown dict with operation, unit_cost, resource_count, total_cost
|
|
34
49
|
"""
|
|
35
|
-
from
|
|
50
|
+
from services.plugin.deps import get_database
|
|
36
51
|
|
|
37
52
|
pricing = get_pricing_service()
|
|
38
53
|
cost_data = pricing.calculate_api_cost('google_maps', action, resource_count)
|
|
39
54
|
|
|
40
55
|
# Save to database
|
|
41
|
-
db =
|
|
56
|
+
db = get_database()
|
|
42
57
|
await db.save_api_usage_metric({
|
|
43
58
|
'session_id': session_id,
|
|
44
59
|
'node_id': node_id,
|
|
@@ -6,7 +6,7 @@ from typing import Any, Dict, Literal, Optional
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, ConfigDict, Field
|
|
8
8
|
|
|
9
|
-
from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
|
|
9
|
+
from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
|
|
10
10
|
|
|
11
11
|
from ._credentials import GoogleMapsCredential
|
|
12
12
|
|
|
@@ -61,12 +61,12 @@ class GmapsCreateNode(ActionNode):
|
|
|
61
61
|
|
|
62
62
|
@Operation("create")
|
|
63
63
|
async def create(self, ctx: NodeContext, params: GmapsCreateParams) -> Any:
|
|
64
|
-
from
|
|
64
|
+
from services.plugin.deps import get_maps_service
|
|
65
65
|
|
|
66
|
-
maps_service =
|
|
66
|
+
maps_service = get_maps_service()
|
|
67
67
|
response = await maps_service.create_map(
|
|
68
68
|
ctx.node_id, params.model_dump(), ctx.raw,
|
|
69
69
|
)
|
|
70
70
|
if response.get("success"):
|
|
71
71
|
return response.get("result") or response
|
|
72
|
-
raise
|
|
72
|
+
raise NodeUserError(response.get("error") or "Map create failed")
|
|
@@ -6,7 +6,7 @@ from typing import Any, Literal, Optional
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, ConfigDict, Field
|
|
8
8
|
|
|
9
|
-
from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
|
|
9
|
+
from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
|
|
10
10
|
|
|
11
11
|
from ._credentials import GoogleMapsCredential
|
|
12
12
|
|
|
@@ -72,12 +72,12 @@ class GmapsLocationsNode(ActionNode):
|
|
|
72
72
|
|
|
73
73
|
@Operation("geocode", cost={"service": "google_maps", "action": "geocode", "count": 1})
|
|
74
74
|
async def geocode(self, ctx: NodeContext, params: GmapsLocationsParams) -> Any:
|
|
75
|
-
from
|
|
75
|
+
from services.plugin.deps import get_maps_service
|
|
76
76
|
|
|
77
|
-
maps_service =
|
|
77
|
+
maps_service = get_maps_service()
|
|
78
78
|
response = await maps_service.geocode_location(
|
|
79
79
|
ctx.node_id, params.model_dump(), ctx.raw,
|
|
80
80
|
)
|
|
81
81
|
if response.get("success"):
|
|
82
82
|
return response.get("result") or response
|
|
83
|
-
raise
|
|
83
|
+
raise NodeUserError(response.get("error") or "Geocoding failed")
|
|
@@ -6,7 +6,7 @@ from typing import Any, Optional
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, ConfigDict, Field
|
|
8
8
|
|
|
9
|
-
from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
|
|
9
|
+
from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
|
|
10
10
|
|
|
11
11
|
from ._credentials import GoogleMapsCredential
|
|
12
12
|
|
|
@@ -51,12 +51,12 @@ class GmapsNearbyPlacesNode(ActionNode):
|
|
|
51
51
|
|
|
52
52
|
@Operation("nearby", cost={"service": "google_maps", "action": "places_nearby", "count": 1})
|
|
53
53
|
async def nearby(self, ctx: NodeContext, params: GmapsNearbyPlacesParams) -> Any:
|
|
54
|
-
from
|
|
54
|
+
from services.plugin.deps import get_maps_service
|
|
55
55
|
|
|
56
|
-
maps_service =
|
|
56
|
+
maps_service = get_maps_service()
|
|
57
57
|
response = await maps_service.find_nearby_places(
|
|
58
58
|
ctx.node_id, params.model_dump(), ctx.raw,
|
|
59
59
|
)
|
|
60
60
|
if response.get("success"):
|
|
61
61
|
return response.get("result") or response
|
|
62
|
-
raise
|
|
62
|
+
raise NodeUserError(response.get("error") or "Nearby places failed")
|
|
@@ -97,9 +97,14 @@ class ChatModelBase(ActionNode, abstract=True):
|
|
|
97
97
|
|
|
98
98
|
@Operation("chat", cost={"service": "chat_model", "action": "chat", "count": 1})
|
|
99
99
|
async def chat(self, ctx: NodeContext, params: ChatModelParams) -> Any:
|
|
100
|
-
from
|
|
101
|
-
|
|
102
|
-
ai_service =
|
|
100
|
+
from services.plugin.deps import get_ai_service
|
|
101
|
+
|
|
102
|
+
ai_service = get_ai_service()
|
|
103
|
+
# ``execute_chat`` raises ``NodeUserError`` directly for typed
|
|
104
|
+
# openai SDK failures (context overflow, bad key, server down) —
|
|
105
|
+
# BaseNode.execute() then logs at WARN with no traceback.
|
|
106
|
+
# ``RuntimeError`` here only fires for "success=False" responses
|
|
107
|
+
# that aren't typed SDK errors (real bugs worth a stacktrace).
|
|
103
108
|
response = await ai_service.execute_chat(
|
|
104
109
|
ctx.node_id, self.type, params.model_dump(),
|
|
105
110
|
)
|
|
@@ -1,26 +1,64 @@
|
|
|
1
1
|
"""LLM provider credentials (Wave 11.E.1 — per-domain).
|
|
2
2
|
|
|
3
|
-
One :class:`ApiKeyCredential` per provider. Used by the
|
|
3
|
+
One :class:`ApiKeyCredential` per provider. Used by the chat-model
|
|
4
4
|
plugins in this folder (openai, anthropic, gemini, openrouter, groq,
|
|
5
|
-
cerebras, deepseek, kimi, mistral) plus the xAI
|
|
6
|
-
by agent plugins. At execution time the plugin's
|
|
7
|
-
SDK client pulls the key directly from
|
|
8
|
-
class is the Credentials-modal + discovery
|
|
9
|
-
client.
|
|
5
|
+
cerebras, deepseek, kimi, mistral, ollama, lmstudio) plus the xAI
|
|
6
|
+
credential referenced by agent plugins. At execution time the plugin's
|
|
7
|
+
LangChain / native SDK client pulls the key directly from
|
|
8
|
+
:mod:`services.auth`; this class is the Credentials-modal + discovery
|
|
9
|
+
manifest, not the runtime client.
|
|
10
|
+
|
|
11
|
+
Local servers (Ollama, LM Studio) follow the same shape as the cloud
|
|
12
|
+
credentials but their api_key is optional — many users run them on
|
|
13
|
+
localhost with no auth. The existing ``{provider}_proxy`` mechanism
|
|
14
|
+
in :func:`services.ai.AIService.create_model` already handles the
|
|
15
|
+
"override base_url + use placeholder api_key" path; the credential
|
|
16
|
+
class only needs to return a placeholder when nothing is stored so
|
|
17
|
+
the central "API key is required" check in ``execute_chat`` passes.
|
|
10
18
|
"""
|
|
11
19
|
|
|
12
20
|
from __future__ import annotations
|
|
13
21
|
|
|
14
|
-
from
|
|
22
|
+
from typing import Any, Dict
|
|
23
|
+
|
|
24
|
+
from services.plugin.credential import ApiKeyCredential, ProbeResult
|
|
15
25
|
|
|
16
26
|
|
|
17
27
|
class _LLMApiKey(ApiKeyCredential):
|
|
18
|
-
"""Shared defaults. Subclasses only set id / display_name / icon.
|
|
28
|
+
"""Shared defaults. Subclasses only set id / display_name / icon.
|
|
29
|
+
|
|
30
|
+
The :meth:`_probe` override calls ``ai_service.fetch_models`` —
|
|
31
|
+
every cloud LLM provider in this file inherits it, so adding a new
|
|
32
|
+
OpenAI-compatible provider is purely declarative (id + base_url in
|
|
33
|
+
JSON; no validator code). The local-server credential override
|
|
34
|
+
(:class:`_LocalLLM`) supersedes ``validate`` entirely because its
|
|
35
|
+
side-effect ordering differs (URL stored under ``{id}_proxy``
|
|
36
|
+
before the probe + per-model context registration after).
|
|
37
|
+
"""
|
|
19
38
|
|
|
20
39
|
category = "AI"
|
|
21
40
|
key_name = "Authorization"
|
|
22
41
|
key_location = "bearer"
|
|
23
42
|
|
|
43
|
+
@classmethod
|
|
44
|
+
async def _probe(cls, api_key: str) -> ProbeResult:
|
|
45
|
+
"""Default LLM probe: fetch the provider's model list.
|
|
46
|
+
|
|
47
|
+
Hits ``GET /v1/models`` (or the provider equivalent) via
|
|
48
|
+
:meth:`AIService.fetch_models`. Returns a populated
|
|
49
|
+
:class:`ProbeResult` on success; raises ``httpx``/``openai``
|
|
50
|
+
exceptions for the base ``Credential.validate`` to classify.
|
|
51
|
+
"""
|
|
52
|
+
from services.plugin.deps import get_ai_service
|
|
53
|
+
|
|
54
|
+
ai_service = get_ai_service()
|
|
55
|
+
models = await ai_service.fetch_models(cls.id, api_key)
|
|
56
|
+
return ProbeResult(
|
|
57
|
+
valid=True,
|
|
58
|
+
message="API key validated",
|
|
59
|
+
models=models,
|
|
60
|
+
)
|
|
61
|
+
|
|
24
62
|
|
|
25
63
|
class OpenAICredential(_LLMApiKey):
|
|
26
64
|
id = "openai"
|
|
@@ -95,3 +133,53 @@ class XaiCredential(_LLMApiKey):
|
|
|
95
133
|
display_name = "xAI (Grok)"
|
|
96
134
|
icon = "asset:xai"
|
|
97
135
|
docs_url = "https://console.x.ai"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _LocalLLM(_LLMApiKey):
|
|
139
|
+
"""Base for local-server credentials (Ollama, LM Studio).
|
|
140
|
+
|
|
141
|
+
Same shape as :class:`_LLMApiKey`, but ``resolve()`` returns the
|
|
142
|
+
documented Ollama placeholder when no key is stored instead of
|
|
143
|
+
raising. The user's custom server address rides on the existing
|
|
144
|
+
``{id}_proxy`` credential — :func:`services.ai.AIService.create_model`
|
|
145
|
+
already reads it and OpenAIProvider already overrides ``base_url``
|
|
146
|
+
+ forces ``api_key="ollama"``. Nothing else to wire.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
async def resolve(cls, *, user_id: str = "owner") -> Dict[str, Any]:
|
|
151
|
+
from services.plugin.deps import get_auth_service
|
|
152
|
+
|
|
153
|
+
api_key = await get_auth_service().get_api_key(cls.id)
|
|
154
|
+
return {"api_key": api_key or "ollama"}
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
async def validate(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
158
|
+
"""Probe the user's local server via the official SDK.
|
|
159
|
+
|
|
160
|
+
Overrides the base ``Credential.validate`` because local-LLM
|
|
161
|
+
side-effect ordering genuinely differs from the cloud case:
|
|
162
|
+
the user's URL is persisted under ``{cls.id}_proxy`` BEFORE
|
|
163
|
+
the probe runs, the placeholder ``api_key="ollama"`` is
|
|
164
|
+
stored under ``cls.id`` only on success, and per-model context
|
|
165
|
+
is registered in the model registry. Delegates to the
|
|
166
|
+
SDK-typed probe in ``_local_validator.py`` which already owns
|
|
167
|
+
that full flow.
|
|
168
|
+
"""
|
|
169
|
+
from ._local_validator import validate_local_llm
|
|
170
|
+
|
|
171
|
+
return await validate_local_llm(dict(data, provider=cls.id))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class OllamaCredential(_LocalLLM):
|
|
175
|
+
id = "ollama"
|
|
176
|
+
display_name = "Ollama"
|
|
177
|
+
icon = "lobehub:ollama"
|
|
178
|
+
docs_url = "https://ollama.com/download"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class LMStudioCredential(_LocalLLM):
|
|
182
|
+
id = "lmstudio"
|
|
183
|
+
display_name = "LM Studio"
|
|
184
|
+
icon = "lobehub:lmstudio"
|
|
185
|
+
docs_url = "https://lmstudio.ai/docs/local-server"
|