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
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Twitter/X OAuth 2.0 callback and API routes.
|
|
3
|
-
|
|
4
|
-
OAuth flow:
|
|
5
|
-
1. Frontend calls WebSocket 'twitter_oauth_login' handler
|
|
6
|
-
2. Backend generates authorization URL, opens browser
|
|
7
|
-
3. User authorizes on Twitter
|
|
8
|
-
4. Twitter redirects to /api/twitter/callback with code
|
|
9
|
-
5. Backend exchanges code for tokens, stores them via auth_service
|
|
10
|
-
6. Frontend polls WebSocket 'twitter_oauth_status' for completion
|
|
11
|
-
|
|
12
|
-
Tokens stored as API keys with provider prefixes:
|
|
13
|
-
- twitter_access_token: OAuth access token (expires in 2 hours)
|
|
14
|
-
- twitter_refresh_token: OAuth refresh token (for token renewal)
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from typing import Optional
|
|
18
|
-
|
|
19
|
-
from fastapi import APIRouter, Query, Request
|
|
20
|
-
from fastapi.responses import HTMLResponse
|
|
21
|
-
|
|
22
|
-
from core.container import container
|
|
23
|
-
from core.logging import get_logger
|
|
24
|
-
from services.twitter_oauth import TwitterOAuth, get_pending_state
|
|
25
|
-
|
|
26
|
-
logger = get_logger(__name__)
|
|
27
|
-
router = APIRouter(prefix="/api/twitter", tags=["twitter"])
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def get_auth_service():
|
|
31
|
-
"""Get auth service for API key storage."""
|
|
32
|
-
return container.auth_service()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@router.get("/callback")
|
|
36
|
-
async def twitter_oauth_callback(
|
|
37
|
-
code: Optional[str] = Query(None),
|
|
38
|
-
state: Optional[str] = Query(None),
|
|
39
|
-
error: Optional[str] = Query(None),
|
|
40
|
-
error_description: Optional[str] = Query(None),
|
|
41
|
-
):
|
|
42
|
-
"""
|
|
43
|
-
Handle Twitter OAuth callback.
|
|
44
|
-
|
|
45
|
-
Twitter redirects here after user authorizes (or denies) the app.
|
|
46
|
-
We exchange the code for tokens and store them via auth_service.
|
|
47
|
-
"""
|
|
48
|
-
# Handle authorization denied
|
|
49
|
-
if error:
|
|
50
|
-
logger.warning(f"Twitter OAuth denied: {error} - {error_description}")
|
|
51
|
-
return HTMLResponse(
|
|
52
|
-
content=_callback_html(
|
|
53
|
-
success=False,
|
|
54
|
-
error=error_description or error,
|
|
55
|
-
),
|
|
56
|
-
status_code=200,
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
# Validate required parameters
|
|
60
|
-
if not code or not state:
|
|
61
|
-
logger.error("Twitter OAuth callback missing code or state")
|
|
62
|
-
return HTMLResponse(
|
|
63
|
-
content=_callback_html(
|
|
64
|
-
success=False,
|
|
65
|
-
error="Missing authorization code or state parameter",
|
|
66
|
-
),
|
|
67
|
-
status_code=400,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
# Verify state exists (CSRF protection)
|
|
71
|
-
pending_state = get_pending_state(state)
|
|
72
|
-
if not pending_state:
|
|
73
|
-
logger.error("Twitter OAuth callback with invalid/expired state")
|
|
74
|
-
return HTMLResponse(
|
|
75
|
-
content=_callback_html(
|
|
76
|
-
success=False,
|
|
77
|
-
error="Invalid or expired authorization state. Please try again.",
|
|
78
|
-
),
|
|
79
|
-
status_code=400,
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# Retrieve redirect_uri from state (set during auth initiation)
|
|
83
|
-
redirect_uri = pending_state.get("redirect_uri")
|
|
84
|
-
if not redirect_uri:
|
|
85
|
-
logger.error("Twitter OAuth callback missing redirect_uri in state")
|
|
86
|
-
return HTMLResponse(
|
|
87
|
-
content=_callback_html(
|
|
88
|
-
success=False,
|
|
89
|
-
error="Invalid state: missing redirect URI. Please try again.",
|
|
90
|
-
),
|
|
91
|
-
status_code=400,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
# Get stored client credentials to create OAuth instance
|
|
95
|
-
auth_service = get_auth_service()
|
|
96
|
-
client_id = await auth_service.get_api_key("twitter_client_id") or ""
|
|
97
|
-
client_secret = await auth_service.get_api_key("twitter_client_secret")
|
|
98
|
-
|
|
99
|
-
oauth = TwitterOAuth(
|
|
100
|
-
client_id=client_id,
|
|
101
|
-
client_secret=client_secret,
|
|
102
|
-
redirect_uri=redirect_uri,
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
# Exchange code for tokens
|
|
106
|
-
result = await oauth.exchange_code(code=code, state=state)
|
|
107
|
-
|
|
108
|
-
if not result.get("success"):
|
|
109
|
-
logger.error(f"Twitter token exchange failed: {result.get('error')}")
|
|
110
|
-
return HTMLResponse(
|
|
111
|
-
content=_callback_html(
|
|
112
|
-
success=False,
|
|
113
|
-
error=result.get("error", "Token exchange failed"),
|
|
114
|
-
),
|
|
115
|
-
status_code=400,
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
# Get user info to display and store
|
|
119
|
-
access_token = result.get("access_token")
|
|
120
|
-
refresh_token = result.get("refresh_token")
|
|
121
|
-
user_info = await oauth.get_user_info(access_token)
|
|
122
|
-
|
|
123
|
-
if not user_info.get("success"):
|
|
124
|
-
logger.warning(f"Failed to get Twitter user info: {user_info.get('error')}")
|
|
125
|
-
username = "Unknown"
|
|
126
|
-
else:
|
|
127
|
-
username = user_info.get("username", "Unknown")
|
|
128
|
-
|
|
129
|
-
# Store encrypted OAuth tokens via auth_service
|
|
130
|
-
await auth_service.store_oauth_tokens(
|
|
131
|
-
provider="twitter",
|
|
132
|
-
access_token=access_token,
|
|
133
|
-
refresh_token=refresh_token or "",
|
|
134
|
-
email=f"@{username}", # Use @username as identifier (Twitter doesn't provide email)
|
|
135
|
-
name=user_info.get("name", "") if user_info.get("success") else "",
|
|
136
|
-
scopes=",".join(result.get("scope", "").split()) if result.get("scope") else "",
|
|
137
|
-
customer_id="owner",
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
# Broadcast completion event to frontend
|
|
141
|
-
from services.status_broadcaster import get_status_broadcaster
|
|
142
|
-
broadcaster = get_status_broadcaster()
|
|
143
|
-
|
|
144
|
-
await broadcaster.broadcast({
|
|
145
|
-
"type": "twitter_oauth_complete",
|
|
146
|
-
"data": {
|
|
147
|
-
"success": True,
|
|
148
|
-
"username": username,
|
|
149
|
-
"user_id": user_info.get("id"),
|
|
150
|
-
"name": user_info.get("name"),
|
|
151
|
-
"profile_image_url": user_info.get("profile_image_url"),
|
|
152
|
-
}
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
logger.info(f"Twitter OAuth successful for @{username}")
|
|
156
|
-
|
|
157
|
-
return HTMLResponse(
|
|
158
|
-
content=_callback_html(
|
|
159
|
-
success=True,
|
|
160
|
-
username=username,
|
|
161
|
-
),
|
|
162
|
-
status_code=200,
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
@router.get("/status")
|
|
167
|
-
async def get_twitter_status(request: Request):
|
|
168
|
-
"""
|
|
169
|
-
Get Twitter connection status.
|
|
170
|
-
|
|
171
|
-
Returns whether the user is authenticated with Twitter.
|
|
172
|
-
"""
|
|
173
|
-
from services.oauth_utils import get_redirect_uri
|
|
174
|
-
|
|
175
|
-
auth_service = get_auth_service()
|
|
176
|
-
|
|
177
|
-
# Try to get stored tokens via auth_service
|
|
178
|
-
tokens = await auth_service.get_oauth_tokens("twitter", customer_id="owner")
|
|
179
|
-
|
|
180
|
-
if not tokens:
|
|
181
|
-
return {
|
|
182
|
-
"connected": False,
|
|
183
|
-
"username": None,
|
|
184
|
-
"user_id": None,
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
access_token = tokens.get("access_token")
|
|
188
|
-
refresh_token = tokens.get("refresh_token")
|
|
189
|
-
|
|
190
|
-
# Get stored client credentials (these remain in auth_service as they're app-level)
|
|
191
|
-
client_id = await auth_service.get_api_key("twitter_client_id") or ""
|
|
192
|
-
client_secret = await auth_service.get_api_key("twitter_client_secret")
|
|
193
|
-
redirect_uri = get_redirect_uri(request, "twitter")
|
|
194
|
-
|
|
195
|
-
oauth = TwitterOAuth(
|
|
196
|
-
client_id=client_id,
|
|
197
|
-
client_secret=client_secret,
|
|
198
|
-
redirect_uri=redirect_uri,
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
# Verify token is still valid by getting user info
|
|
202
|
-
user_info = await oauth.get_user_info(access_token)
|
|
203
|
-
|
|
204
|
-
if not user_info.get("success"):
|
|
205
|
-
# Token may be expired, try to refresh
|
|
206
|
-
if refresh_token:
|
|
207
|
-
refresh_result = await oauth.refresh_access_token(refresh_token)
|
|
208
|
-
if refresh_result.get("success"):
|
|
209
|
-
# Store new tokens via auth_service
|
|
210
|
-
await auth_service.store_oauth_tokens(
|
|
211
|
-
provider="twitter",
|
|
212
|
-
access_token=refresh_result["access_token"],
|
|
213
|
-
refresh_token=refresh_result.get("refresh_token") or refresh_token,
|
|
214
|
-
email=tokens.get("email"),
|
|
215
|
-
name=tokens.get("name"),
|
|
216
|
-
scopes=tokens.get("scopes"),
|
|
217
|
-
customer_id="owner",
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
# Retry user info
|
|
221
|
-
user_info = await oauth.get_user_info(refresh_result["access_token"])
|
|
222
|
-
|
|
223
|
-
if not user_info.get("success"):
|
|
224
|
-
return {
|
|
225
|
-
"connected": False,
|
|
226
|
-
"username": None,
|
|
227
|
-
"user_id": None,
|
|
228
|
-
"error": user_info.get("error"),
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
"connected": True,
|
|
233
|
-
"username": user_info.get("username"),
|
|
234
|
-
"user_id": user_info.get("id"),
|
|
235
|
-
"name": user_info.get("name"),
|
|
236
|
-
"profile_image_url": user_info.get("profile_image_url"),
|
|
237
|
-
"verified": user_info.get("verified"),
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
@router.post("/logout")
|
|
242
|
-
async def twitter_logout(request: Request):
|
|
243
|
-
"""
|
|
244
|
-
Disconnect Twitter by revoking tokens and clearing stored credentials.
|
|
245
|
-
"""
|
|
246
|
-
from services.oauth_utils import get_redirect_uri
|
|
247
|
-
|
|
248
|
-
auth_service = get_auth_service()
|
|
249
|
-
|
|
250
|
-
# Get stored tokens via auth_service
|
|
251
|
-
tokens = await auth_service.get_oauth_tokens("twitter", customer_id="owner")
|
|
252
|
-
|
|
253
|
-
if tokens:
|
|
254
|
-
access_token = tokens.get("access_token")
|
|
255
|
-
refresh_token = tokens.get("refresh_token")
|
|
256
|
-
|
|
257
|
-
# Get client credentials for revocation (app-level, in auth_service)
|
|
258
|
-
client_id = await auth_service.get_api_key("twitter_client_id") or ""
|
|
259
|
-
client_secret = await auth_service.get_api_key("twitter_client_secret")
|
|
260
|
-
redirect_uri = get_redirect_uri(request, "twitter")
|
|
261
|
-
|
|
262
|
-
oauth = TwitterOAuth(
|
|
263
|
-
client_id=client_id,
|
|
264
|
-
client_secret=client_secret,
|
|
265
|
-
redirect_uri=redirect_uri,
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
# Revoke tokens
|
|
269
|
-
if access_token:
|
|
270
|
-
await oauth.revoke_token(access_token, "access_token")
|
|
271
|
-
|
|
272
|
-
if refresh_token:
|
|
273
|
-
await oauth.revoke_token(refresh_token, "refresh_token")
|
|
274
|
-
|
|
275
|
-
# Clear stored credentials via auth_service
|
|
276
|
-
await auth_service.remove_oauth_tokens("twitter", customer_id="owner")
|
|
277
|
-
|
|
278
|
-
logger.info("Twitter disconnected and tokens revoked")
|
|
279
|
-
|
|
280
|
-
return {"success": True, "message": "Twitter disconnected"}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def _callback_html(success: bool, username: str = None, error: str = None) -> str:
|
|
284
|
-
"""Generate callback HTML page that closes itself and notifies parent."""
|
|
285
|
-
if success:
|
|
286
|
-
title = "Twitter Connected"
|
|
287
|
-
message = f"Successfully connected as @{username}!"
|
|
288
|
-
color = "#00ba7c" # Green
|
|
289
|
-
else:
|
|
290
|
-
title = "Connection Failed"
|
|
291
|
-
message = error or "Failed to connect to Twitter"
|
|
292
|
-
color = "#f4212e" # Red
|
|
293
|
-
|
|
294
|
-
escaped_error = error.replace("'", "\\'") if error else ""
|
|
295
|
-
return f"""
|
|
296
|
-
<!DOCTYPE html>
|
|
297
|
-
<html>
|
|
298
|
-
<head>
|
|
299
|
-
<title>{title}</title>
|
|
300
|
-
<style>
|
|
301
|
-
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
302
|
-
body {{
|
|
303
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
304
|
-
background: linear-gradient(135deg, #15202b 0%, #1a1a2e 100%);
|
|
305
|
-
min-height: 100vh;
|
|
306
|
-
display: flex;
|
|
307
|
-
align-items: center;
|
|
308
|
-
justify-content: center;
|
|
309
|
-
color: #fff;
|
|
310
|
-
}}
|
|
311
|
-
.container {{
|
|
312
|
-
text-align: center;
|
|
313
|
-
padding: 40px;
|
|
314
|
-
background: rgba(255, 255, 255, 0.05);
|
|
315
|
-
border-radius: 16px;
|
|
316
|
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
317
|
-
max-width: 400px;
|
|
318
|
-
}}
|
|
319
|
-
.icon {{
|
|
320
|
-
width: 64px;
|
|
321
|
-
height: 64px;
|
|
322
|
-
margin-bottom: 20px;
|
|
323
|
-
color: {color};
|
|
324
|
-
}}
|
|
325
|
-
h1 {{
|
|
326
|
-
font-size: 24px;
|
|
327
|
-
margin-bottom: 12px;
|
|
328
|
-
color: {color};
|
|
329
|
-
}}
|
|
330
|
-
p {{
|
|
331
|
-
font-size: 16px;
|
|
332
|
-
color: rgba(255, 255, 255, 0.8);
|
|
333
|
-
margin-bottom: 20px;
|
|
334
|
-
}}
|
|
335
|
-
.close-text {{
|
|
336
|
-
font-size: 14px;
|
|
337
|
-
color: rgba(255, 255, 255, 0.5);
|
|
338
|
-
}}
|
|
339
|
-
</style>
|
|
340
|
-
</head>
|
|
341
|
-
<body>
|
|
342
|
-
<div class="container">
|
|
343
|
-
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
344
|
-
{"<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z'/>" if success else "<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z'/>"}
|
|
345
|
-
</svg>
|
|
346
|
-
<h1>{title}</h1>
|
|
347
|
-
<p>{message}</p>
|
|
348
|
-
<p class="close-text">This window will close automatically...</p>
|
|
349
|
-
</div>
|
|
350
|
-
<script>
|
|
351
|
-
// Notify parent window and close
|
|
352
|
-
if (window.opener) {{
|
|
353
|
-
window.opener.postMessage({{
|
|
354
|
-
type: 'twitter_oauth_callback',
|
|
355
|
-
success: {str(success).lower()},
|
|
356
|
-
{"username: '" + username + "'," if username else ""}
|
|
357
|
-
{"error: '" + escaped_error + "'," if error else ""}
|
|
358
|
-
}}, '*');
|
|
359
|
-
}}
|
|
360
|
-
// Close after 2 seconds
|
|
361
|
-
setTimeout(function() {{ window.close(); }}, 2000);
|
|
362
|
-
</script>
|
|
363
|
-
</body>
|
|
364
|
-
</html>
|
|
365
|
-
"""
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"""Claude Code service - manages CLI execution with session persistence."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import shutil
|
|
7
|
-
from typing import Any, Dict, List, Optional
|
|
8
|
-
|
|
9
|
-
from core.logging import get_logger
|
|
10
|
-
|
|
11
|
-
logger = get_logger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _find_claude_cmd() -> List[str]:
|
|
15
|
-
"""Find claude CLI cross-platform."""
|
|
16
|
-
if shutil.which("claude"):
|
|
17
|
-
return ["claude"]
|
|
18
|
-
npx = shutil.which("npx")
|
|
19
|
-
if npx:
|
|
20
|
-
return [npx, "-y", "@anthropic-ai/claude-code"]
|
|
21
|
-
raise FileNotFoundError("Neither 'claude' nor 'npx' found in PATH")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ClaudeCodeService:
|
|
25
|
-
"""Executes tasks via Claude Code CLI with session tracking."""
|
|
26
|
-
|
|
27
|
-
def __init__(self):
|
|
28
|
-
self._session_map: Dict[str, str] = {} # node_id -> session_id
|
|
29
|
-
|
|
30
|
-
async def execute(
|
|
31
|
-
self,
|
|
32
|
-
prompt: str,
|
|
33
|
-
node_id: str = "",
|
|
34
|
-
model: str = "claude-sonnet-4-6",
|
|
35
|
-
cwd: Optional[str] = None,
|
|
36
|
-
allowed_tools: str = "Read,Edit,Bash,Glob,Grep,Write",
|
|
37
|
-
max_turns: int = 10,
|
|
38
|
-
max_budget_usd: float = 5.0,
|
|
39
|
-
system_prompt: Optional[str] = None,
|
|
40
|
-
) -> Dict[str, Any]:
|
|
41
|
-
"""Run claude CLI and return parsed JSON result."""
|
|
42
|
-
cmd = _find_claude_cmd() + [
|
|
43
|
-
"-p", prompt,
|
|
44
|
-
"--output-format", "json",
|
|
45
|
-
"--model", model,
|
|
46
|
-
"--max-turns", str(max_turns),
|
|
47
|
-
"--allowedTools", allowed_tools,
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
if max_budget_usd > 0:
|
|
51
|
-
cmd += ["--max-budget-usd", str(max_budget_usd)]
|
|
52
|
-
|
|
53
|
-
# Resume existing session for this node
|
|
54
|
-
session_id = self._session_map.get(node_id)
|
|
55
|
-
if session_id:
|
|
56
|
-
cmd += ["--resume", session_id]
|
|
57
|
-
|
|
58
|
-
if system_prompt:
|
|
59
|
-
cmd += ["--append-system-prompt", system_prompt]
|
|
60
|
-
|
|
61
|
-
if not cwd:
|
|
62
|
-
from core.config import Settings
|
|
63
|
-
cwd = os.path.join(Settings().workspace_base_resolved, 'default')
|
|
64
|
-
os.makedirs(cwd, exist_ok=True)
|
|
65
|
-
work_dir = cwd
|
|
66
|
-
|
|
67
|
-
proc = await asyncio.create_subprocess_exec(
|
|
68
|
-
*cmd,
|
|
69
|
-
stdout=asyncio.subprocess.PIPE,
|
|
70
|
-
stderr=asyncio.subprocess.PIPE,
|
|
71
|
-
cwd=work_dir,
|
|
72
|
-
)
|
|
73
|
-
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300)
|
|
74
|
-
|
|
75
|
-
if proc.returncode != 0:
|
|
76
|
-
err = stderr.decode("utf-8", errors="replace").strip()
|
|
77
|
-
logger.error("Claude Code failed (exit %d): %s", proc.returncode, err)
|
|
78
|
-
raise RuntimeError(err or f"Exit code {proc.returncode}")
|
|
79
|
-
|
|
80
|
-
raw = stdout.decode("utf-8", errors="replace").strip()
|
|
81
|
-
try:
|
|
82
|
-
data = json.loads(raw)
|
|
83
|
-
except json.JSONDecodeError:
|
|
84
|
-
data = {"result": raw}
|
|
85
|
-
|
|
86
|
-
# Track session for future resume
|
|
87
|
-
if data.get("session_id") and node_id:
|
|
88
|
-
self._session_map[node_id] = data["session_id"]
|
|
89
|
-
|
|
90
|
-
return data
|
|
91
|
-
|
|
92
|
-
def get_session_id(self, node_id: str) -> Optional[str]:
|
|
93
|
-
return self._session_map.get(node_id)
|
|
94
|
-
|
|
95
|
-
def clear_session(self, node_id: str) -> None:
|
|
96
|
-
self._session_map.pop(node_id, None)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
_instance: Optional[ClaudeCodeService] = None
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def get_claude_code_service() -> ClaudeCodeService:
|
|
103
|
-
global _instance
|
|
104
|
-
if _instance is None:
|
|
105
|
-
_instance = ClaudeCodeService()
|
|
106
|
-
return _instance
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
"""Markdown-based conversation memory helpers.
|
|
2
|
-
|
|
3
|
-
Parse, append, trim, and archive conversation history stored in markdown format.
|
|
4
|
-
Used by AI Agent and Chat Agent for persistent memory across turns.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import re
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
from typing import Dict, Any, List
|
|
10
|
-
|
|
11
|
-
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage
|
|
12
|
-
|
|
13
|
-
from core.logging import get_logger
|
|
14
|
-
|
|
15
|
-
logger = get_logger(__name__)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def parse_memory_markdown(content: str) -> List[BaseMessage]:
|
|
19
|
-
"""Parse markdown memory content into LangChain messages.
|
|
20
|
-
|
|
21
|
-
Markdown format:
|
|
22
|
-
### **Human** (timestamp)
|
|
23
|
-
message content
|
|
24
|
-
|
|
25
|
-
### **Assistant** (timestamp)
|
|
26
|
-
response content
|
|
27
|
-
"""
|
|
28
|
-
messages = []
|
|
29
|
-
pattern = r'### \*\*(Human|Assistant)\*\*[^\n]*\n(.*?)(?=\n### \*\*|$)'
|
|
30
|
-
for role, text in re.findall(pattern, content, re.DOTALL):
|
|
31
|
-
text = text.strip()
|
|
32
|
-
if text:
|
|
33
|
-
msg_class = HumanMessage if role == 'Human' else AIMessage
|
|
34
|
-
messages.append(msg_class(content=text))
|
|
35
|
-
return messages
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def append_to_memory_markdown(content: str, role: str, message: str) -> str:
|
|
39
|
-
"""Append a message to markdown memory content."""
|
|
40
|
-
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
41
|
-
label = "Human" if role == "human" else "Assistant"
|
|
42
|
-
entry = f"\n### **{label}** ({ts})\n{message}\n"
|
|
43
|
-
# Remove empty state message if present
|
|
44
|
-
return content.replace("*No messages yet.*\n", "") + entry
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def trim_markdown_window(content: str, window_size: int) -> tuple:
|
|
48
|
-
"""Keep last N message pairs, return (trimmed_content, removed_texts).
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
content: Full markdown content
|
|
52
|
-
window_size: Number of message PAIRS to keep (human+assistant)
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
Tuple of (trimmed markdown, list of removed message texts for archival)
|
|
56
|
-
"""
|
|
57
|
-
pattern = r'(### \*\*(Human|Assistant)\*\*[^\n]*\n.*?)(?=\n### \*\*|$)'
|
|
58
|
-
blocks = [m[0] for m in re.findall(pattern, content, re.DOTALL)]
|
|
59
|
-
|
|
60
|
-
if len(blocks) <= window_size * 2:
|
|
61
|
-
return content, []
|
|
62
|
-
|
|
63
|
-
keep = blocks[-(window_size * 2):]
|
|
64
|
-
removed = blocks[:-(window_size * 2)]
|
|
65
|
-
|
|
66
|
-
# Extract text from removed blocks for vector storage
|
|
67
|
-
removed_texts = []
|
|
68
|
-
for block in removed:
|
|
69
|
-
match = re.search(r'\n(.*)$', block, re.DOTALL)
|
|
70
|
-
if match:
|
|
71
|
-
removed_texts.append(match.group(1).strip())
|
|
72
|
-
|
|
73
|
-
return "# Conversation History\n" + "\n".join(keep), removed_texts
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
# Global cache for vector stores per session (InMemoryVectorStore)
|
|
77
|
-
_memory_vector_stores: Dict[str, Any] = {}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def get_memory_vector_store(session_id: str):
|
|
81
|
-
"""Get or create InMemoryVectorStore for a session."""
|
|
82
|
-
if session_id not in _memory_vector_stores:
|
|
83
|
-
try:
|
|
84
|
-
from langchain_core.vectorstores import InMemoryVectorStore
|
|
85
|
-
from langchain_huggingface import HuggingFaceEmbeddings
|
|
86
|
-
|
|
87
|
-
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-en-v1.5")
|
|
88
|
-
_memory_vector_stores[session_id] = InMemoryVectorStore(embeddings)
|
|
89
|
-
logger.debug(f"[Memory] Created vector store for session '{session_id}'")
|
|
90
|
-
except ImportError as e:
|
|
91
|
-
logger.warning(f"[Memory] Vector store not available: {e}")
|
|
92
|
-
return None
|
|
93
|
-
return _memory_vector_stores[session_id]
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def clear_agent_session_state(
|
|
97
|
-
session_id: str,
|
|
98
|
-
workflow_id: str = None,
|
|
99
|
-
clear_long_term: bool = False,
|
|
100
|
-
) -> Dict[str, Any]:
|
|
101
|
-
"""Clear every store keyed by an agent's conversational scope.
|
|
102
|
-
|
|
103
|
-
"Memory" from the user's perspective is not just the markdown
|
|
104
|
-
transcript — it's every piece of state an agent reuses across
|
|
105
|
-
iterations of a conversation. ``simpleMemory.memory_content`` is the
|
|
106
|
-
visible part; the long-term vector store and ``TodoService``
|
|
107
|
-
plan-work-update lists are the invisible parts that quietly bloat
|
|
108
|
-
subsequent runs (notably ``task_agent``, whose default skill bundle
|
|
109
|
-
instructs the LLM to read accumulated todos every run).
|
|
110
|
-
|
|
111
|
-
``TodoService`` is keyed by ``ctx.workflow_id or ctx.node_id or
|
|
112
|
-
"default"`` (see ``server/nodes/tool/write_todos.py``). We clear all
|
|
113
|
-
three candidate keys to match whichever fallback the agent actually
|
|
114
|
-
used at write time.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
session_id: ``simpleMemory`` node's ``session_id`` parameter.
|
|
118
|
-
workflow_id: Active workflow id (passed by the frontend so we
|
|
119
|
-
can clear ``TodoService`` entries written under it).
|
|
120
|
-
clear_long_term: When ``True``, drop the per-session vector
|
|
121
|
-
store too.
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
Dict with ``cleared_vector_store`` (bool) and ``cleared_todo_keys``
|
|
125
|
-
(list[str]) for caller-visible diagnostics. Markdown reset is
|
|
126
|
-
signalled by returning the default content, owned by the WS
|
|
127
|
-
handler so the wire shape stays in one place.
|
|
128
|
-
"""
|
|
129
|
-
# The live vector-store cache lives in ``services.ai`` (the dict in
|
|
130
|
-
# this module is dormant — nothing imports it). Lazy import keeps
|
|
131
|
-
# ``services.ai``'s heavy LangChain deps off the hot path.
|
|
132
|
-
from services.ai import _memory_vector_stores as _live_vector_stores
|
|
133
|
-
from services.todo_service import get_todo_service
|
|
134
|
-
|
|
135
|
-
cleared_vector_store = False
|
|
136
|
-
if clear_long_term and session_id in _live_vector_stores:
|
|
137
|
-
del _live_vector_stores[session_id]
|
|
138
|
-
cleared_vector_store = True
|
|
139
|
-
logger.info(f"[Memory] Cleared vector store for session '{session_id}'")
|
|
140
|
-
|
|
141
|
-
todo_service = get_todo_service()
|
|
142
|
-
cleared_todo_keys: List[str] = []
|
|
143
|
-
seen = set()
|
|
144
|
-
for key in (workflow_id, session_id, "default"):
|
|
145
|
-
if key and key not in seen:
|
|
146
|
-
seen.add(key)
|
|
147
|
-
todo_service.clear(key)
|
|
148
|
-
cleared_todo_keys.append(key)
|
|
149
|
-
|
|
150
|
-
logger.info(
|
|
151
|
-
"[Memory] Cleared agent session state session=%s workflow_id=%s "
|
|
152
|
-
"vector_store=%s todo_keys=%s",
|
|
153
|
-
session_id, workflow_id, cleared_vector_store, cleared_todo_keys,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
"cleared_vector_store": cleared_vector_store,
|
|
158
|
-
"cleared_todo_keys": cleared_todo_keys,
|
|
159
|
-
}
|