machinaos 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.template +71 -0
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/bin/cli.js +159 -0
- package/client/.dockerignore +45 -0
- package/client/Dockerfile +68 -0
- package/client/eslint.config.js +29 -0
- package/client/index.html +13 -0
- package/client/nginx.conf +66 -0
- package/client/package.json +48 -0
- package/client/src/App.tsx +27 -0
- package/client/src/Dashboard.tsx +1173 -0
- package/client/src/ParameterPanel.tsx +301 -0
- package/client/src/components/AIAgentNode.tsx +321 -0
- package/client/src/components/APIKeyValidator.tsx +118 -0
- package/client/src/components/ClaudeChatModelNode.tsx +18 -0
- package/client/src/components/ConditionalEdge.tsx +189 -0
- package/client/src/components/CredentialsModal.tsx +306 -0
- package/client/src/components/EdgeConditionEditor.tsx +443 -0
- package/client/src/components/GeminiChatModelNode.tsx +18 -0
- package/client/src/components/GenericNode.tsx +357 -0
- package/client/src/components/LocationParameterPanel.tsx +154 -0
- package/client/src/components/ModelNode.tsx +286 -0
- package/client/src/components/OpenAIChatModelNode.tsx +18 -0
- package/client/src/components/OutputPanel.tsx +471 -0
- package/client/src/components/ParameterRenderer.tsx +1874 -0
- package/client/src/components/SkillEditorModal.tsx +417 -0
- package/client/src/components/SquareNode.tsx +797 -0
- package/client/src/components/StartNode.tsx +250 -0
- package/client/src/components/ToolkitNode.tsx +365 -0
- package/client/src/components/TriggerNode.tsx +463 -0
- package/client/src/components/auth/LoginPage.tsx +247 -0
- package/client/src/components/auth/ProtectedRoute.tsx +59 -0
- package/client/src/components/base/BaseChatModelNode.tsx +271 -0
- package/client/src/components/icons/AIProviderIcons.tsx +50 -0
- package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
- package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
- package/client/src/components/maps/index.ts +26 -0
- package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
- package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
- package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
- package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
- package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
- package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
- package/client/src/components/parameterPanel/index.ts +42 -0
- package/client/src/components/shared/DataPanel.tsx +142 -0
- package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
- package/client/src/components/ui/AIResultModal.tsx +204 -0
- package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
- package/client/src/components/ui/CodeEditor.tsx +81 -0
- package/client/src/components/ui/CollapsibleSection.tsx +88 -0
- package/client/src/components/ui/ComponentItem.tsx +154 -0
- package/client/src/components/ui/ComponentPalette.tsx +321 -0
- package/client/src/components/ui/ConsolePanel.tsx +1074 -0
- package/client/src/components/ui/ErrorBoundary.tsx +196 -0
- package/client/src/components/ui/InputNodesPanel.tsx +204 -0
- package/client/src/components/ui/MapSelector.tsx +314 -0
- package/client/src/components/ui/Modal.tsx +149 -0
- package/client/src/components/ui/NodeContextMenu.tsx +192 -0
- package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
- package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
- package/client/src/components/ui/SettingsPanel.tsx +243 -0
- package/client/src/components/ui/TopToolbar.tsx +736 -0
- package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
- package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
- package/client/src/config/antdTheme.ts +186 -0
- package/client/src/config/api.ts +54 -0
- package/client/src/contexts/AuthContext.tsx +221 -0
- package/client/src/contexts/ThemeContext.tsx +42 -0
- package/client/src/contexts/WebSocketContext.tsx +1971 -0
- package/client/src/factories/baseChatModelFactory.ts +256 -0
- package/client/src/hooks/useAndroidOperations.ts +164 -0
- package/client/src/hooks/useApiKeyValidation.ts +107 -0
- package/client/src/hooks/useApiKeys.ts +238 -0
- package/client/src/hooks/useAppTheme.ts +17 -0
- package/client/src/hooks/useComponentPalette.ts +51 -0
- package/client/src/hooks/useCopyPaste.ts +155 -0
- package/client/src/hooks/useDragAndDrop.ts +124 -0
- package/client/src/hooks/useDragVariable.ts +88 -0
- package/client/src/hooks/useExecution.ts +313 -0
- package/client/src/hooks/useParameterPanel.ts +176 -0
- package/client/src/hooks/useReactFlowNodes.ts +189 -0
- package/client/src/hooks/useToolSchema.ts +209 -0
- package/client/src/hooks/useWhatsApp.ts +196 -0
- package/client/src/hooks/useWorkflowManagement.ts +46 -0
- package/client/src/index.css +315 -0
- package/client/src/main.tsx +19 -0
- package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
- package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
- package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
- package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
- package/client/src/nodeDefinitions/chatNodes.ts +135 -0
- package/client/src/nodeDefinitions/codeNodes.ts +54 -0
- package/client/src/nodeDefinitions/documentNodes.ts +379 -0
- package/client/src/nodeDefinitions/index.ts +15 -0
- package/client/src/nodeDefinitions/locationNodes.ts +463 -0
- package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
- package/client/src/nodeDefinitions/skillNodes.ts +211 -0
- package/client/src/nodeDefinitions/toolNodes.ts +198 -0
- package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
- package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
- package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
- package/client/src/nodeDefinitions.ts +104 -0
- package/client/src/schemas/workflowSchema.ts +264 -0
- package/client/src/services/dynamicParameterService.ts +96 -0
- package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
- package/client/src/services/executionService.ts +232 -0
- package/client/src/services/workflowApi.ts +91 -0
- package/client/src/store/useAppStore.ts +582 -0
- package/client/src/styles/theme.ts +508 -0
- package/client/src/styles/zIndex.ts +17 -0
- package/client/src/types/ComponentTypes.ts +39 -0
- package/client/src/types/EdgeCondition.ts +231 -0
- package/client/src/types/INodeProperties.ts +288 -0
- package/client/src/types/NodeTypes.ts +28 -0
- package/client/src/utils/formatters.ts +33 -0
- package/client/src/utils/googleMapsLoader.ts +140 -0
- package/client/src/utils/locationUtils.ts +85 -0
- package/client/src/utils/nodeUtils.ts +31 -0
- package/client/src/utils/workflow.ts +30 -0
- package/client/src/utils/workflowExport.ts +120 -0
- package/client/src/vite-env.d.ts +12 -0
- package/client/tailwind.config.js +60 -0
- package/client/tsconfig.json +25 -0
- package/client/tsconfig.node.json +11 -0
- package/client/vite.config.js +35 -0
- package/docker-compose.prod.yml +107 -0
- package/docker-compose.yml +104 -0
- package/docs-MachinaOs/README.md +85 -0
- package/docs-MachinaOs/deployment/docker.mdx +228 -0
- package/docs-MachinaOs/deployment/production.mdx +345 -0
- package/docs-MachinaOs/docs.json +75 -0
- package/docs-MachinaOs/faq.mdx +309 -0
- package/docs-MachinaOs/favicon.svg +5 -0
- package/docs-MachinaOs/installation.mdx +160 -0
- package/docs-MachinaOs/introduction.mdx +114 -0
- package/docs-MachinaOs/logo/dark.svg +6 -0
- package/docs-MachinaOs/logo/light.svg +6 -0
- package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
- package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
- package/docs-MachinaOs/nodes/android.mdx +411 -0
- package/docs-MachinaOs/nodes/overview.mdx +181 -0
- package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
- package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
- package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
- package/docs-MachinaOs/quickstart.mdx +119 -0
- package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
- package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
- package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
- package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
- package/nul +0 -0
- package/package.json +70 -0
- package/scripts/build.js +158 -0
- package/scripts/check-ports.ps1 +33 -0
- package/scripts/clean.js +40 -0
- package/scripts/docker.js +93 -0
- package/scripts/kill-port.ps1 +154 -0
- package/scripts/start.js +210 -0
- package/scripts/stop.js +325 -0
- package/server/.dockerignore +44 -0
- package/server/Dockerfile +45 -0
- package/server/constants.py +249 -0
- package/server/core/__init__.py +1 -0
- package/server/core/cache.py +461 -0
- package/server/core/config.py +128 -0
- package/server/core/container.py +99 -0
- package/server/core/database.py +1211 -0
- package/server/core/logging.py +314 -0
- package/server/main.py +289 -0
- package/server/middleware/__init__.py +5 -0
- package/server/middleware/auth.py +89 -0
- package/server/models/__init__.py +1 -0
- package/server/models/auth.py +52 -0
- package/server/models/cache.py +24 -0
- package/server/models/database.py +211 -0
- package/server/models/nodes.py +455 -0
- package/server/package.json +9 -0
- package/server/pyproject.toml +72 -0
- package/server/requirements.txt +83 -0
- package/server/routers/__init__.py +1 -0
- package/server/routers/android.py +294 -0
- package/server/routers/auth.py +203 -0
- package/server/routers/database.py +151 -0
- package/server/routers/maps.py +142 -0
- package/server/routers/nodejs_compat.py +289 -0
- package/server/routers/webhook.py +90 -0
- package/server/routers/websocket.py +2127 -0
- package/server/routers/whatsapp.py +761 -0
- package/server/routers/workflow.py +200 -0
- package/server/services/__init__.py +1 -0
- package/server/services/ai.py +2415 -0
- package/server/services/android/__init__.py +27 -0
- package/server/services/android/broadcaster.py +114 -0
- package/server/services/android/client.py +608 -0
- package/server/services/android/manager.py +78 -0
- package/server/services/android/protocol.py +165 -0
- package/server/services/android_service.py +588 -0
- package/server/services/auth.py +131 -0
- package/server/services/chat_client.py +160 -0
- package/server/services/deployment/__init__.py +12 -0
- package/server/services/deployment/manager.py +706 -0
- package/server/services/deployment/state.py +47 -0
- package/server/services/deployment/triggers.py +275 -0
- package/server/services/event_waiter.py +785 -0
- package/server/services/execution/__init__.py +77 -0
- package/server/services/execution/cache.py +769 -0
- package/server/services/execution/conditions.py +373 -0
- package/server/services/execution/dlq.py +132 -0
- package/server/services/execution/executor.py +1351 -0
- package/server/services/execution/models.py +531 -0
- package/server/services/execution/recovery.py +235 -0
- package/server/services/handlers/__init__.py +126 -0
- package/server/services/handlers/ai.py +355 -0
- package/server/services/handlers/android.py +260 -0
- package/server/services/handlers/code.py +278 -0
- package/server/services/handlers/document.py +598 -0
- package/server/services/handlers/http.py +193 -0
- package/server/services/handlers/polyglot.py +105 -0
- package/server/services/handlers/tools.py +845 -0
- package/server/services/handlers/triggers.py +107 -0
- package/server/services/handlers/utility.py +822 -0
- package/server/services/handlers/whatsapp.py +476 -0
- package/server/services/maps.py +289 -0
- package/server/services/memory_store.py +103 -0
- package/server/services/node_executor.py +375 -0
- package/server/services/parameter_resolver.py +218 -0
- package/server/services/polyglot_client.py +169 -0
- package/server/services/scheduler.py +155 -0
- package/server/services/skill_loader.py +417 -0
- package/server/services/status_broadcaster.py +826 -0
- package/server/services/temporal/__init__.py +23 -0
- package/server/services/temporal/activities.py +344 -0
- package/server/services/temporal/client.py +76 -0
- package/server/services/temporal/executor.py +147 -0
- package/server/services/temporal/worker.py +251 -0
- package/server/services/temporal/workflow.py +355 -0
- package/server/services/temporal/ws_client.py +236 -0
- package/server/services/text.py +111 -0
- package/server/services/user_auth.py +172 -0
- package/server/services/websocket_client.py +29 -0
- package/server/services/workflow.py +597 -0
- package/server/skills/android-skill/SKILL.md +82 -0
- package/server/skills/assistant-personality/SKILL.md +45 -0
- package/server/skills/code-skill/SKILL.md +140 -0
- package/server/skills/http-skill/SKILL.md +161 -0
- package/server/skills/maps-skill/SKILL.md +170 -0
- package/server/skills/memory-skill/SKILL.md +154 -0
- package/server/skills/scheduler-skill/SKILL.md +84 -0
- package/server/skills/whatsapp-skill/SKILL.md +283 -0
- package/server/uv.lock +2916 -0
- package/server/whatsapp-rpc/.dockerignore +30 -0
- package/server/whatsapp-rpc/Dockerfile +44 -0
- package/server/whatsapp-rpc/Dockerfile.web +17 -0
- package/server/whatsapp-rpc/README.md +139 -0
- package/server/whatsapp-rpc/cli.js +95 -0
- package/server/whatsapp-rpc/configs/config.yaml +7 -0
- package/server/whatsapp-rpc/docker-compose.yml +35 -0
- package/server/whatsapp-rpc/docs/API.md +410 -0
- package/server/whatsapp-rpc/go.mod +67 -0
- package/server/whatsapp-rpc/go.sum +203 -0
- package/server/whatsapp-rpc/package.json +30 -0
- package/server/whatsapp-rpc/schema.json +1294 -0
- package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
- package/server/whatsapp-rpc/scripts/cli.js +162 -0
- package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
- package/server/whatsapp-rpc/src/go/config/config.go +49 -0
- package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
- package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
- package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
- package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
- package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
- package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
- package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
- package/server/whatsapp-rpc/web/app.py +609 -0
- package/server/whatsapp-rpc/web/requirements.txt +6 -0
- package/server/whatsapp-rpc/web/rpc_client.py +427 -0
- package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
- package/server/whatsapp-rpc/web/templates/base.html +150 -0
- package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
- package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
- package/server/whatsapp-rpc/web/templates/groups.html +328 -0
- package/server/whatsapp-rpc/web/templates/messages.html +465 -0
- package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
- package/server/whatsapp-rpc/web/templates/send.html +259 -0
- package/server/whatsapp-rpc/web/templates/settings.html +459 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Temporal workflow orchestration service.
|
|
2
|
+
|
|
3
|
+
This module provides Temporal integration for durable distributed workflow execution.
|
|
4
|
+
|
|
5
|
+
Architecture:
|
|
6
|
+
- Each workflow node executes as an independent Temporal activity
|
|
7
|
+
- Activities can run on ANY worker in the cluster for horizontal scaling
|
|
8
|
+
- Workflow only orchestrates - schedules activities and routes outputs
|
|
9
|
+
- WebSocket connection to MachinaOs for low-latency node execution
|
|
10
|
+
|
|
11
|
+
When TEMPORAL_ENABLED=true:
|
|
12
|
+
- Workflows are executed via Temporal for durability and distribution
|
|
13
|
+
- Each node is a separate activity with its own retry policy
|
|
14
|
+
- Parallel branches execute concurrently on available workers
|
|
15
|
+
|
|
16
|
+
When TEMPORAL_ENABLED=false (default):
|
|
17
|
+
- Falls back to the existing parallel/sequential executor
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .executor import TemporalExecutor
|
|
21
|
+
from .client import TemporalClientWrapper
|
|
22
|
+
|
|
23
|
+
__all__ = ["TemporalExecutor", "TemporalClientWrapper"]
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Temporal activities for distributed node execution.
|
|
2
|
+
|
|
3
|
+
Uses class-based activity pattern recommended by Temporal docs for sharing
|
|
4
|
+
resources like aiohttp.ClientSession across activity invocations.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
- https://docs.temporal.io/develop/python/python-sdk-sync-vs-async
|
|
8
|
+
- https://docs.temporal.io/develop/python/core-application
|
|
9
|
+
|
|
10
|
+
Architecture:
|
|
11
|
+
- NodeExecutionActivities class holds shared aiohttp.ClientSession
|
|
12
|
+
- Session is passed via constructor, avoiding recreation per activity
|
|
13
|
+
- Each activity call gets its own WebSocket connection from the session pool
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Any, Dict, Optional
|
|
18
|
+
|
|
19
|
+
import aiohttp
|
|
20
|
+
from temporalio import activity
|
|
21
|
+
|
|
22
|
+
from core.logging import get_logger
|
|
23
|
+
from core.config import Settings
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
# Load settings to get the correct server port
|
|
28
|
+
_settings = Settings()
|
|
29
|
+
MACHINA_URL = f"http://{_settings.host}:{_settings.port}"
|
|
30
|
+
WS_URL = f"ws://{_settings.host}:{_settings.port}/ws/internal"
|
|
31
|
+
|
|
32
|
+
print(f"[Temporal Activities] MACHINA_URL configured: {MACHINA_URL}")
|
|
33
|
+
print(f"[Temporal Activities] WS_URL configured: {WS_URL}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NodeExecutionActivities:
|
|
37
|
+
"""Activity class for node execution with shared aiohttp session.
|
|
38
|
+
|
|
39
|
+
Following Temporal's recommended pattern for dependency injection:
|
|
40
|
+
- aiohttp.ClientSession is passed via constructor
|
|
41
|
+
- Session provides connection pooling for concurrent activities
|
|
42
|
+
- Each activity call gets its own WebSocket connection from the pool
|
|
43
|
+
|
|
44
|
+
Reference: https://docs.temporal.io/develop/python/python-sdk-sync-vs-async
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, session: aiohttp.ClientSession):
|
|
48
|
+
"""Initialize with shared aiohttp session.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
session: aiohttp.ClientSession with connection pooling configured
|
|
52
|
+
"""
|
|
53
|
+
self.session = session
|
|
54
|
+
self.ws_url = WS_URL
|
|
55
|
+
self.http_url = f"{MACHINA_URL}/api/workflow/node/execute"
|
|
56
|
+
self.broadcast_url = f"{MACHINA_URL}/api/workflow/broadcast-status"
|
|
57
|
+
|
|
58
|
+
@activity.defn
|
|
59
|
+
async def execute_node_activity(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
60
|
+
"""Execute a single workflow node with isolated context.
|
|
61
|
+
|
|
62
|
+
This activity can run on ANY worker in the cluster, enabling
|
|
63
|
+
horizontal scaling and multi-tenant distribution.
|
|
64
|
+
|
|
65
|
+
Each node execution is independent - if it fails, Temporal will retry
|
|
66
|
+
on the same or different worker without affecting other nodes.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
context: Immutable context containing:
|
|
70
|
+
- node_id: Unique node identifier
|
|
71
|
+
- node_type: Type of node (aiAgent, console, timer, etc.)
|
|
72
|
+
- node_data: Node configuration from React Flow
|
|
73
|
+
- inputs: Outputs from upstream nodes (dependencies)
|
|
74
|
+
- workflow_id: Parent workflow ID for tracking
|
|
75
|
+
- tenant_id: Tenant identifier for multi-tenancy
|
|
76
|
+
- session_id: Session identifier
|
|
77
|
+
- nodes: Full node list (for tool/memory detection by handlers)
|
|
78
|
+
- edges: Full edge list (for tool/memory detection by handlers)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dict with success, result, node_id, and metadata
|
|
82
|
+
"""
|
|
83
|
+
node_id = context["node_id"]
|
|
84
|
+
node_type = context["node_type"]
|
|
85
|
+
node_data = context.get("node_data", {})
|
|
86
|
+
workflow_id = context.get("workflow_id")
|
|
87
|
+
tenant_id = context.get("tenant_id")
|
|
88
|
+
|
|
89
|
+
activity.logger.info(
|
|
90
|
+
f"Executing node activity: {node_id} ({node_type})",
|
|
91
|
+
extra={"tenant_id": tenant_id, "workflow_id": workflow_id},
|
|
92
|
+
)
|
|
93
|
+
print(f"[Activity] Starting node: {node_id} (type={node_type})")
|
|
94
|
+
|
|
95
|
+
# Heartbeat at start to signal activity is alive
|
|
96
|
+
activity.heartbeat(f"Starting {node_type}: {node_id}")
|
|
97
|
+
|
|
98
|
+
# Handle pre-executed trigger nodes (already have their output)
|
|
99
|
+
if context.get("pre_executed"):
|
|
100
|
+
print(f"[Activity] Node {node_id} is pre-executed, returning cached result")
|
|
101
|
+
result = {
|
|
102
|
+
"success": True,
|
|
103
|
+
"node_id": node_id,
|
|
104
|
+
"node_type": node_type,
|
|
105
|
+
"result": context.get("trigger_output", {}),
|
|
106
|
+
"pre_executed": True,
|
|
107
|
+
"timestamp": datetime.now().isoformat(),
|
|
108
|
+
}
|
|
109
|
+
await self._broadcast_status(node_id, "success", result, workflow_id)
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
# Handle disabled nodes
|
|
113
|
+
if node_data.get("disabled"):
|
|
114
|
+
print(f"[Activity] Node {node_id} is disabled, skipping")
|
|
115
|
+
result = {
|
|
116
|
+
"success": True,
|
|
117
|
+
"node_id": node_id,
|
|
118
|
+
"node_type": node_type,
|
|
119
|
+
"skipped": True,
|
|
120
|
+
"reason": "disabled",
|
|
121
|
+
"timestamp": datetime.now().isoformat(),
|
|
122
|
+
}
|
|
123
|
+
await self._broadcast_status(node_id, "skipped", {"disabled": True}, workflow_id)
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
# Broadcast "executing" status for UI updates
|
|
127
|
+
await self._broadcast_status(
|
|
128
|
+
node_id=node_id,
|
|
129
|
+
status="executing",
|
|
130
|
+
data={"node_type": node_type},
|
|
131
|
+
workflow_id=workflow_id,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# Heartbeat before potentially long WebSocket operation
|
|
136
|
+
activity.heartbeat(f"Executing via WebSocket: {node_id}")
|
|
137
|
+
|
|
138
|
+
# Execute node via WebSocket (each call gets own connection from pool)
|
|
139
|
+
result = await self._execute_via_websocket(context)
|
|
140
|
+
|
|
141
|
+
# Add metadata
|
|
142
|
+
result["node_id"] = node_id
|
|
143
|
+
result["node_type"] = node_type
|
|
144
|
+
result["timestamp"] = datetime.now().isoformat()
|
|
145
|
+
|
|
146
|
+
# Broadcast result status
|
|
147
|
+
if result.get("success"):
|
|
148
|
+
await self._broadcast_status(
|
|
149
|
+
node_id=node_id,
|
|
150
|
+
status="success",
|
|
151
|
+
data={
|
|
152
|
+
"result": result.get("result"),
|
|
153
|
+
"execution_time": result.get("execution_time"),
|
|
154
|
+
},
|
|
155
|
+
workflow_id=workflow_id,
|
|
156
|
+
)
|
|
157
|
+
print(f"[Activity] Node {node_id} completed successfully")
|
|
158
|
+
else:
|
|
159
|
+
await self._broadcast_status(
|
|
160
|
+
node_id=node_id,
|
|
161
|
+
status="error",
|
|
162
|
+
data={"error": result.get("error")},
|
|
163
|
+
workflow_id=workflow_id,
|
|
164
|
+
)
|
|
165
|
+
print(f"[Activity] Node {node_id} failed: {result.get('error')}")
|
|
166
|
+
|
|
167
|
+
# Heartbeat for activity liveness
|
|
168
|
+
activity.heartbeat(f"Node {node_id} completed")
|
|
169
|
+
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
error_msg = f"{type(e).__name__}: {str(e)}"
|
|
174
|
+
logger.error(f"Node {node_id} execution failed: {error_msg}")
|
|
175
|
+
print(f"[Activity] Node {node_id} EXCEPTION: {error_msg}")
|
|
176
|
+
|
|
177
|
+
# Broadcast error status
|
|
178
|
+
await self._broadcast_status(
|
|
179
|
+
node_id=node_id,
|
|
180
|
+
status="error",
|
|
181
|
+
data={"error": error_msg},
|
|
182
|
+
workflow_id=workflow_id,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Raise to trigger Temporal retry mechanism
|
|
186
|
+
raise
|
|
187
|
+
|
|
188
|
+
async def _execute_via_websocket(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
189
|
+
"""Execute node via WebSocket using shared session's connection pool.
|
|
190
|
+
|
|
191
|
+
Each call creates a new WebSocket connection from the session's pool,
|
|
192
|
+
avoiding race conditions when multiple activities run concurrently.
|
|
193
|
+
"""
|
|
194
|
+
import json
|
|
195
|
+
import uuid
|
|
196
|
+
|
|
197
|
+
node_id = context["node_id"]
|
|
198
|
+
node_type = context["node_type"]
|
|
199
|
+
request_id = str(uuid.uuid4())
|
|
200
|
+
|
|
201
|
+
message = {
|
|
202
|
+
"type": "execute_node",
|
|
203
|
+
"request_id": request_id,
|
|
204
|
+
"node_id": node_id,
|
|
205
|
+
"node_type": node_type,
|
|
206
|
+
"parameters": context.get("node_data", {}),
|
|
207
|
+
"nodes": context.get("nodes", []),
|
|
208
|
+
"edges": context.get("edges", []),
|
|
209
|
+
"session_id": context.get("session_id", "default"),
|
|
210
|
+
"workflow_id": context.get("workflow_id"),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
print(f"[Activity] WebSocket execute for {node_id}")
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Each activity gets its own WebSocket connection from the pool
|
|
217
|
+
async with self.session.ws_connect(
|
|
218
|
+
self.ws_url,
|
|
219
|
+
heartbeat=20,
|
|
220
|
+
receive_timeout=120,
|
|
221
|
+
) as ws:
|
|
222
|
+
await ws.send_json(message)
|
|
223
|
+
print(f"[Activity] Sent request for {node_id}")
|
|
224
|
+
|
|
225
|
+
# Wait for response with matching request_id
|
|
226
|
+
async for msg in ws:
|
|
227
|
+
if msg.type == aiohttp.WSMsgType.TEXT:
|
|
228
|
+
response = json.loads(msg.data)
|
|
229
|
+
if response.get("request_id") == request_id:
|
|
230
|
+
print(f"[Activity] Got response for {node_id}: success={response.get('success')}")
|
|
231
|
+
return response
|
|
232
|
+
elif msg.type == aiohttp.WSMsgType.ERROR:
|
|
233
|
+
raise Exception(f"WebSocket error: {ws.exception()}")
|
|
234
|
+
elif msg.type == aiohttp.WSMsgType.CLOSED:
|
|
235
|
+
raise Exception("WebSocket closed unexpectedly")
|
|
236
|
+
|
|
237
|
+
raise Exception(f"No response for request {request_id}")
|
|
238
|
+
|
|
239
|
+
except aiohttp.ClientError as e:
|
|
240
|
+
raise Exception(f"WebSocket connection error: {e}")
|
|
241
|
+
|
|
242
|
+
async def _broadcast_status(
|
|
243
|
+
self,
|
|
244
|
+
node_id: str,
|
|
245
|
+
status: str,
|
|
246
|
+
data: dict,
|
|
247
|
+
workflow_id: str = None,
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Broadcast node status for real-time UI updates.
|
|
250
|
+
|
|
251
|
+
Non-fatal - execution continues even if broadcast fails.
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
async with self.session.post(
|
|
255
|
+
self.broadcast_url,
|
|
256
|
+
json={
|
|
257
|
+
"node_id": node_id,
|
|
258
|
+
"status": status,
|
|
259
|
+
"data": data or {},
|
|
260
|
+
"workflow_id": workflow_id,
|
|
261
|
+
},
|
|
262
|
+
timeout=aiohttp.ClientTimeout(total=5),
|
|
263
|
+
) as response:
|
|
264
|
+
if response.status == 200:
|
|
265
|
+
print(f"[Activity] Broadcast: {node_id} -> {status}")
|
|
266
|
+
except Exception as e:
|
|
267
|
+
# Non-fatal - don't fail execution if broadcast fails
|
|
268
|
+
logger.warning(f"Broadcast failed for {node_id}: {e}")
|
|
269
|
+
print(f"[Activity] Broadcast failed (non-fatal): {e}")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# =============================================================================
|
|
273
|
+
# Factory function for creating activity instance with session
|
|
274
|
+
# =============================================================================
|
|
275
|
+
|
|
276
|
+
def create_node_activities(session: aiohttp.ClientSession) -> NodeExecutionActivities:
|
|
277
|
+
"""Factory function to create activity instance with shared session.
|
|
278
|
+
|
|
279
|
+
This follows Temporal's recommended pattern for dependency injection.
|
|
280
|
+
The session should be created once when the worker starts and reused.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
session: aiohttp.ClientSession with connection pooling
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
NodeExecutionActivities instance ready for worker registration
|
|
287
|
+
"""
|
|
288
|
+
return NodeExecutionActivities(session)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
async def create_shared_session(pool_size: int = 100) -> aiohttp.ClientSession:
|
|
292
|
+
"""Create a shared aiohttp session with connection pooling.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
pool_size: Maximum number of concurrent connections
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Configured aiohttp.ClientSession
|
|
299
|
+
"""
|
|
300
|
+
connector = aiohttp.TCPConnector(
|
|
301
|
+
limit=pool_size,
|
|
302
|
+
limit_per_host=pool_size,
|
|
303
|
+
enable_cleanup_closed=True,
|
|
304
|
+
)
|
|
305
|
+
timeout = aiohttp.ClientTimeout(
|
|
306
|
+
total=300, # 5 min total
|
|
307
|
+
connect=10, # 10 sec connect
|
|
308
|
+
)
|
|
309
|
+
session = aiohttp.ClientSession(
|
|
310
|
+
connector=connector,
|
|
311
|
+
timeout=timeout,
|
|
312
|
+
)
|
|
313
|
+
print(f"[Activities] Created shared session with pool_size={pool_size}")
|
|
314
|
+
return session
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# =============================================================================
|
|
318
|
+
# Standalone activity function (for backwards compatibility)
|
|
319
|
+
# =============================================================================
|
|
320
|
+
|
|
321
|
+
# Global session for standalone function
|
|
322
|
+
_global_session: Optional[aiohttp.ClientSession] = None
|
|
323
|
+
_global_activities: Optional[NodeExecutionActivities] = None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
async def _get_global_activities() -> NodeExecutionActivities:
|
|
327
|
+
"""Get or create global activities instance."""
|
|
328
|
+
global _global_session, _global_activities
|
|
329
|
+
|
|
330
|
+
if _global_session is None or _global_session.closed:
|
|
331
|
+
_global_session = await create_shared_session()
|
|
332
|
+
_global_activities = NodeExecutionActivities(_global_session)
|
|
333
|
+
|
|
334
|
+
return _global_activities
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@activity.defn
|
|
338
|
+
async def execute_node_activity(context: Dict[str, Any]) -> Dict[str, Any]:
|
|
339
|
+
"""Standalone activity function for backwards compatibility.
|
|
340
|
+
|
|
341
|
+
For new code, use NodeExecutionActivities class with shared session.
|
|
342
|
+
"""
|
|
343
|
+
activities = await _get_global_activities()
|
|
344
|
+
return await activities.execute_node_activity(context)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Temporal client wrapper for MachinaOs.
|
|
2
|
+
|
|
3
|
+
Manages the Temporal client connection lifecycle.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from temporalio.client import Client
|
|
8
|
+
from temporalio.runtime import Runtime, TelemetryConfig
|
|
9
|
+
|
|
10
|
+
from core.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TemporalClientWrapper:
|
|
16
|
+
"""Wrapper around Temporal client for lifecycle management."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, server_address: str, namespace: str = "default"):
|
|
19
|
+
"""Initialize the client wrapper.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
server_address: Temporal server address (e.g., "localhost:7233")
|
|
23
|
+
namespace: Temporal namespace to use
|
|
24
|
+
"""
|
|
25
|
+
self.server_address = server_address
|
|
26
|
+
self.namespace = namespace
|
|
27
|
+
self._client: Optional[Client] = None
|
|
28
|
+
self._runtime: Optional[Runtime] = None
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def client(self) -> Optional[Client]:
|
|
32
|
+
"""Get the underlying Temporal client."""
|
|
33
|
+
return self._client
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def is_connected(self) -> bool:
|
|
37
|
+
"""Check if client is connected."""
|
|
38
|
+
return self._client is not None
|
|
39
|
+
|
|
40
|
+
async def connect(self) -> Client:
|
|
41
|
+
"""Connect to the Temporal server.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The connected Temporal client
|
|
45
|
+
"""
|
|
46
|
+
if self._client is not None:
|
|
47
|
+
return self._client
|
|
48
|
+
|
|
49
|
+
logger.info(
|
|
50
|
+
"Connecting to Temporal server",
|
|
51
|
+
server_address=self.server_address,
|
|
52
|
+
namespace=self.namespace,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Create runtime with worker heartbeating disabled to avoid warning on older servers
|
|
56
|
+
self._runtime = Runtime(
|
|
57
|
+
telemetry=TelemetryConfig(),
|
|
58
|
+
worker_heartbeat_interval=None,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self._client = await Client.connect(
|
|
62
|
+
self.server_address,
|
|
63
|
+
namespace=self.namespace,
|
|
64
|
+
runtime=self._runtime,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
logger.info("Connected to Temporal server")
|
|
68
|
+
return self._client
|
|
69
|
+
|
|
70
|
+
async def disconnect(self) -> None:
|
|
71
|
+
"""Disconnect from the Temporal server."""
|
|
72
|
+
if self._client is not None:
|
|
73
|
+
# Temporal client doesn't have an explicit close method,
|
|
74
|
+
# but we clear the reference
|
|
75
|
+
self._client = None
|
|
76
|
+
logger.info("Disconnected from Temporal server")
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Temporal executor for MachinaOs workflow execution.
|
|
2
|
+
|
|
3
|
+
Provides the same interface as WorkflowExecutor but delegates
|
|
4
|
+
execution to Temporal for durable workflow orchestration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
11
|
+
|
|
12
|
+
from temporalio.client import Client
|
|
13
|
+
|
|
14
|
+
from core.logging import get_logger
|
|
15
|
+
from .workflow import MachinaWorkflow
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TemporalExecutor:
|
|
21
|
+
"""Workflow executor that uses Temporal for durable execution.
|
|
22
|
+
|
|
23
|
+
Provides a compatible interface with the existing WorkflowExecutor
|
|
24
|
+
so it can be used as a drop-in replacement when Temporal is enabled.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
client: Client,
|
|
30
|
+
task_queue: str = "machina-tasks",
|
|
31
|
+
status_callback: Optional[Callable] = None,
|
|
32
|
+
):
|
|
33
|
+
"""Initialize the Temporal executor.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
client: Connected Temporal client
|
|
37
|
+
task_queue: Temporal task queue name
|
|
38
|
+
status_callback: Optional callback for node status updates
|
|
39
|
+
"""
|
|
40
|
+
self.client = client
|
|
41
|
+
self.task_queue = task_queue
|
|
42
|
+
self.status_callback = status_callback
|
|
43
|
+
|
|
44
|
+
async def execute_workflow(
|
|
45
|
+
self,
|
|
46
|
+
workflow_id: str,
|
|
47
|
+
nodes: List[Dict],
|
|
48
|
+
edges: List[Dict],
|
|
49
|
+
session_id: str = "default",
|
|
50
|
+
enable_caching: bool = True,
|
|
51
|
+
) -> Dict[str, Any]:
|
|
52
|
+
"""Execute a workflow using Temporal.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
workflow_id: Unique workflow identifier
|
|
56
|
+
nodes: List of node definitions
|
|
57
|
+
edges: List of edge definitions
|
|
58
|
+
session_id: Session identifier
|
|
59
|
+
enable_caching: Whether to enable result caching (passed to activity)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dict with success, outputs, execution_trace, and timing info
|
|
63
|
+
"""
|
|
64
|
+
start_time = time.time()
|
|
65
|
+
execution_id = f"temporal-{uuid.uuid4().hex[:8]}"
|
|
66
|
+
|
|
67
|
+
logger.info(
|
|
68
|
+
"Starting Temporal workflow execution",
|
|
69
|
+
workflow_id=workflow_id,
|
|
70
|
+
execution_id=execution_id,
|
|
71
|
+
node_count=len(nodes),
|
|
72
|
+
edge_count=len(edges),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# Execute workflow via Temporal
|
|
77
|
+
result = await self.client.execute_workflow(
|
|
78
|
+
MachinaWorkflow.run,
|
|
79
|
+
{
|
|
80
|
+
"nodes": nodes,
|
|
81
|
+
"edges": edges,
|
|
82
|
+
"session_id": session_id,
|
|
83
|
+
"workflow_id": workflow_id,
|
|
84
|
+
},
|
|
85
|
+
id=execution_id,
|
|
86
|
+
task_queue=self.task_queue,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
execution_time = time.time() - start_time
|
|
90
|
+
|
|
91
|
+
# Notify status callback for completed nodes
|
|
92
|
+
if self.status_callback and result.get("success"):
|
|
93
|
+
for node_id in result.get("execution_trace", []):
|
|
94
|
+
try:
|
|
95
|
+
await self.status_callback(
|
|
96
|
+
node_id,
|
|
97
|
+
"completed",
|
|
98
|
+
result.get("outputs", {}).get(node_id, {}),
|
|
99
|
+
)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.warning(
|
|
102
|
+
f"Status callback error for node {node_id}: {e}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
logger.info(
|
|
106
|
+
"Temporal workflow completed",
|
|
107
|
+
workflow_id=workflow_id,
|
|
108
|
+
execution_id=execution_id,
|
|
109
|
+
success=result.get("success"),
|
|
110
|
+
nodes_executed=len(result.get("execution_trace", [])),
|
|
111
|
+
execution_time=execution_time,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"success": result.get("success", False),
|
|
116
|
+
"execution_id": execution_id,
|
|
117
|
+
"nodes_executed": result.get("execution_trace", []),
|
|
118
|
+
"outputs": result.get("outputs", {}),
|
|
119
|
+
"errors": [result.get("error")] if result.get("error") else [],
|
|
120
|
+
"execution_time": execution_time,
|
|
121
|
+
"temporal_execution": True,
|
|
122
|
+
"timestamp": datetime.now().isoformat(),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
import traceback
|
|
127
|
+
execution_time = time.time() - start_time
|
|
128
|
+
error_details = f"{type(e).__name__}: {str(e)}"
|
|
129
|
+
tb = traceback.format_exc()
|
|
130
|
+
logger.error(
|
|
131
|
+
f"Temporal workflow failed: {error_details}",
|
|
132
|
+
workflow_id=workflow_id,
|
|
133
|
+
execution_id=execution_id,
|
|
134
|
+
traceback=tb,
|
|
135
|
+
)
|
|
136
|
+
print(f"[Temporal] Workflow failed with exception:\n{tb}")
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"success": False,
|
|
140
|
+
"execution_id": execution_id,
|
|
141
|
+
"nodes_executed": [],
|
|
142
|
+
"outputs": {},
|
|
143
|
+
"errors": [error_details],
|
|
144
|
+
"execution_time": execution_time,
|
|
145
|
+
"temporal_execution": True,
|
|
146
|
+
"timestamp": datetime.now().isoformat(),
|
|
147
|
+
}
|