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,251 @@
|
|
|
1
|
+
"""Temporal worker for distributed node execution.
|
|
2
|
+
|
|
3
|
+
Uses class-based activities with shared aiohttp session for proper
|
|
4
|
+
connection pooling across concurrent activity executions.
|
|
5
|
+
|
|
6
|
+
The worker polls the task queue and executes:
|
|
7
|
+
- MachinaWorkflow: Orchestrates the graph, schedules node activities
|
|
8
|
+
- NodeExecutionActivities: Executes individual nodes with shared session
|
|
9
|
+
|
|
10
|
+
Multiple workers can be started on different machines for horizontal scaling.
|
|
11
|
+
Each node activity can execute on any available worker in the cluster.
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
- https://docs.temporal.io/develop/python/python-sdk-sync-vs-async
|
|
15
|
+
- https://docs.temporal.io/develop/worker-performance
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
import aiohttp
|
|
22
|
+
from temporalio.client import Client
|
|
23
|
+
from temporalio.runtime import Runtime, TelemetryConfig
|
|
24
|
+
from temporalio.worker import Worker
|
|
25
|
+
|
|
26
|
+
from core.logging import get_logger
|
|
27
|
+
from .workflow import MachinaWorkflow
|
|
28
|
+
print(f"[Worker Import] MachinaWorkflow loaded from: {MachinaWorkflow.__module__}")
|
|
29
|
+
from .activities import (
|
|
30
|
+
NodeExecutionActivities,
|
|
31
|
+
create_shared_session,
|
|
32
|
+
execute_node_activity,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger = get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_runtime() -> Runtime:
|
|
39
|
+
"""Create a Temporal runtime with worker heartbeating disabled.
|
|
40
|
+
|
|
41
|
+
Disables the runtime-level worker heartbeating feature to avoid
|
|
42
|
+
the warning on older Temporal server versions that don't support it.
|
|
43
|
+
"""
|
|
44
|
+
return Runtime(
|
|
45
|
+
telemetry=TelemetryConfig(),
|
|
46
|
+
worker_heartbeat_interval=None, # Disable runtime heartbeating
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TemporalWorkerManager:
|
|
51
|
+
"""Manages the Temporal worker lifecycle with shared resources.
|
|
52
|
+
|
|
53
|
+
Creates a shared aiohttp.ClientSession that is passed to the activity
|
|
54
|
+
class, following Temporal's recommended dependency injection pattern.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
client: Client,
|
|
60
|
+
task_queue: str = "machina-tasks",
|
|
61
|
+
pool_size: int = 100,
|
|
62
|
+
):
|
|
63
|
+
"""Initialize the worker manager.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
client: Connected Temporal client
|
|
67
|
+
task_queue: Task queue name to poll
|
|
68
|
+
pool_size: Connection pool size for aiohttp session
|
|
69
|
+
"""
|
|
70
|
+
self.client = client
|
|
71
|
+
self.task_queue = task_queue
|
|
72
|
+
self.pool_size = pool_size
|
|
73
|
+
self._worker: Optional[Worker] = None
|
|
74
|
+
self._worker_task: Optional[asyncio.Task] = None
|
|
75
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
76
|
+
self._activities: Optional[NodeExecutionActivities] = None
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def is_running(self) -> bool:
|
|
80
|
+
"""Check if the worker is running."""
|
|
81
|
+
return self._worker_task is not None and not self._worker_task.done()
|
|
82
|
+
|
|
83
|
+
async def start(self) -> None:
|
|
84
|
+
"""Start the Temporal worker in the background."""
|
|
85
|
+
if self.is_running:
|
|
86
|
+
logger.warning("Temporal worker already running")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Create shared aiohttp session with connection pooling
|
|
90
|
+
self._session = await create_shared_session(self.pool_size)
|
|
91
|
+
|
|
92
|
+
# Create activity instance with shared session
|
|
93
|
+
self._activities = NodeExecutionActivities(self._session)
|
|
94
|
+
|
|
95
|
+
# Create worker with class-based activity
|
|
96
|
+
# For class-based activities, pass the bound method (instance.method)
|
|
97
|
+
self._worker = Worker(
|
|
98
|
+
self.client,
|
|
99
|
+
task_queue=self.task_queue,
|
|
100
|
+
workflows=[MachinaWorkflow],
|
|
101
|
+
activities=[self._activities.execute_node_activity], # Pass bound method
|
|
102
|
+
# Allow concurrent activity execution for parallel branches
|
|
103
|
+
max_concurrent_activities=self.pool_size,
|
|
104
|
+
max_concurrent_workflow_tasks=10,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
logger.info(
|
|
108
|
+
"Starting Temporal worker",
|
|
109
|
+
task_queue=self.task_queue,
|
|
110
|
+
pool_size=self.pool_size,
|
|
111
|
+
)
|
|
112
|
+
print(f"[Worker] Starting with pool_size={self.pool_size}")
|
|
113
|
+
|
|
114
|
+
# Run worker in background task
|
|
115
|
+
self._worker_task = asyncio.create_task(
|
|
116
|
+
self._run_worker(),
|
|
117
|
+
name="temporal-worker",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
async def _run_worker(self) -> None:
|
|
121
|
+
"""Run the worker (background task)."""
|
|
122
|
+
try:
|
|
123
|
+
await self._worker.run()
|
|
124
|
+
except asyncio.CancelledError:
|
|
125
|
+
logger.info("Temporal worker cancelled")
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(f"Temporal worker error: {str(e)}")
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
async def stop(self) -> None:
|
|
131
|
+
"""Stop the Temporal worker and cleanup resources."""
|
|
132
|
+
if not self.is_running:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
logger.info("Stopping Temporal worker")
|
|
136
|
+
|
|
137
|
+
if self._worker_task:
|
|
138
|
+
self._worker_task.cancel()
|
|
139
|
+
try:
|
|
140
|
+
await self._worker_task
|
|
141
|
+
except asyncio.CancelledError:
|
|
142
|
+
pass
|
|
143
|
+
self._worker_task = None
|
|
144
|
+
|
|
145
|
+
# Close shared session
|
|
146
|
+
if self._session and not self._session.closed:
|
|
147
|
+
await self._session.close()
|
|
148
|
+
print("[Worker] Closed shared session")
|
|
149
|
+
|
|
150
|
+
self._worker = None
|
|
151
|
+
self._session = None
|
|
152
|
+
self._activities = None
|
|
153
|
+
logger.info("Temporal worker stopped")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def run_standalone_worker(
|
|
157
|
+
server_address: str = "localhost:7233",
|
|
158
|
+
namespace: str = "default",
|
|
159
|
+
task_queue: str = "machina-tasks",
|
|
160
|
+
pool_size: int = 100,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Run the Temporal worker as a standalone process.
|
|
163
|
+
|
|
164
|
+
This can be used for running workers separately from the main server,
|
|
165
|
+
enabling horizontal scaling across multiple machines.
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
# Start multiple workers for horizontal scaling
|
|
169
|
+
python -m services.temporal.worker
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
server_address: Temporal server address
|
|
173
|
+
namespace: Temporal namespace
|
|
174
|
+
task_queue: Task queue to poll
|
|
175
|
+
pool_size: Connection pool size
|
|
176
|
+
"""
|
|
177
|
+
logger.info(
|
|
178
|
+
"Starting standalone Temporal worker",
|
|
179
|
+
server_address=server_address,
|
|
180
|
+
namespace=namespace,
|
|
181
|
+
task_queue=task_queue,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
print(f"[Worker] Connecting to {server_address}")
|
|
185
|
+
print(f"[Worker] Namespace: {namespace}")
|
|
186
|
+
print(f"[Worker] Task Queue: {task_queue}")
|
|
187
|
+
print(f"[Worker] Pool Size: {pool_size}")
|
|
188
|
+
|
|
189
|
+
# Use custom runtime with heartbeating disabled to avoid warning on older servers
|
|
190
|
+
runtime = create_runtime()
|
|
191
|
+
client = await Client.connect(server_address, namespace=namespace, runtime=runtime)
|
|
192
|
+
|
|
193
|
+
# Create shared session and activities
|
|
194
|
+
session = await create_shared_session(pool_size)
|
|
195
|
+
activities = NodeExecutionActivities(session)
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
worker = Worker(
|
|
199
|
+
client,
|
|
200
|
+
task_queue=task_queue,
|
|
201
|
+
workflows=[MachinaWorkflow],
|
|
202
|
+
activities=[activities.execute_node_activity], # Pass bound method
|
|
203
|
+
max_concurrent_activities=pool_size,
|
|
204
|
+
max_concurrent_workflow_tasks=10,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
print("[Worker] Running. Press Ctrl+C to stop.")
|
|
208
|
+
logger.info("Worker running. Press Ctrl+C to stop.")
|
|
209
|
+
await worker.run()
|
|
210
|
+
|
|
211
|
+
finally:
|
|
212
|
+
# Cleanup session on shutdown
|
|
213
|
+
if not session.closed:
|
|
214
|
+
await session.close()
|
|
215
|
+
print("[Worker] Session closed")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
async def create_worker(
|
|
219
|
+
client: Client,
|
|
220
|
+
task_queue: str = "machina-tasks",
|
|
221
|
+
session: Optional[aiohttp.ClientSession] = None,
|
|
222
|
+
) -> Worker:
|
|
223
|
+
"""Create a worker instance for use in tests or custom setups.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
client: Connected Temporal client
|
|
227
|
+
task_queue: Task queue name
|
|
228
|
+
session: Optional shared aiohttp session (created if not provided)
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Configured Worker instance (not started)
|
|
232
|
+
"""
|
|
233
|
+
if session is None:
|
|
234
|
+
session = await create_shared_session()
|
|
235
|
+
|
|
236
|
+
activities = NodeExecutionActivities(session)
|
|
237
|
+
|
|
238
|
+
return Worker(
|
|
239
|
+
client,
|
|
240
|
+
task_queue=task_queue,
|
|
241
|
+
workflows=[MachinaWorkflow],
|
|
242
|
+
activities=[activities.execute_node_activity], # Pass bound method
|
|
243
|
+
max_concurrent_activities=100,
|
|
244
|
+
max_concurrent_workflow_tasks=10,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
# Allow running worker standalone
|
|
250
|
+
# Usage: python -m services.temporal.worker
|
|
251
|
+
asyncio.run(run_standalone_worker())
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Temporal workflow - Pure orchestrator for distributed node execution.
|
|
2
|
+
|
|
3
|
+
The workflow ONLY orchestrates:
|
|
4
|
+
- Parses graph structure
|
|
5
|
+
- Filters config nodes (tools, memory, services)
|
|
6
|
+
- Determines execution order based on dependencies
|
|
7
|
+
- Schedules node activities (can run on ANY worker)
|
|
8
|
+
- Collects results and routes outputs to dependent nodes
|
|
9
|
+
|
|
10
|
+
NO business logic in workflow - all execution happens in activities.
|
|
11
|
+
This enables massive horizontal scaling and multi-tenant distribution.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from datetime import timedelta
|
|
15
|
+
from typing import Any, Dict, List, Set
|
|
16
|
+
|
|
17
|
+
from temporalio import workflow
|
|
18
|
+
from temporalio.common import RetryPolicy
|
|
19
|
+
|
|
20
|
+
# Config handles - nodes connecting via these are config nodes (not executed)
|
|
21
|
+
# AI Agent handles: input-memory, input-tools, input-model
|
|
22
|
+
# Chat Agent handles: input-skill, input-tools
|
|
23
|
+
CONFIG_HANDLES = {"input-tools", "input-memory", "input-model", "input-skill"}
|
|
24
|
+
|
|
25
|
+
# Android service types (connect to androidTool, not executed directly)
|
|
26
|
+
ANDROID_SERVICE_TYPES = {
|
|
27
|
+
"batteryMonitor", "locationService", "deviceState",
|
|
28
|
+
"systemInfo", "appList", "appLauncher",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Skill node types (connect to Chat Agent's input-skill, not executed directly)
|
|
32
|
+
SKILL_NODE_TYPES = {
|
|
33
|
+
"assistantPersonality", "whatsappSkill", "memorySkill", "mapsSkill",
|
|
34
|
+
"httpSkill", "schedulerSkill", "androidSkill", "codeSkill", "customSkill",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@workflow.defn(sandboxed=False)
|
|
38
|
+
class MachinaWorkflow:
|
|
39
|
+
"""Distributed workflow orchestrator.
|
|
40
|
+
|
|
41
|
+
This workflow ONLY orchestrates - all execution happens in activities
|
|
42
|
+
that can run on any worker in the cluster.
|
|
43
|
+
|
|
44
|
+
Features:
|
|
45
|
+
- Continuous scheduling (FIRST_COMPLETED pattern)
|
|
46
|
+
- Per-node retry policies
|
|
47
|
+
- Config node filtering (tools, memory, services)
|
|
48
|
+
- Multi-tenant support via tenant_id in context
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@workflow.run
|
|
52
|
+
async def run(self, workflow_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
53
|
+
print("[Workflow] ========== RUN METHOD CALLED ==========")
|
|
54
|
+
"""Execute workflow by orchestrating node activities.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
workflow_data: Dict containing:
|
|
58
|
+
- nodes: List of node definitions from React Flow
|
|
59
|
+
- edges: List of edge definitions from React Flow
|
|
60
|
+
- session_id: Session identifier
|
|
61
|
+
- workflow_id: Workflow ID for tracking
|
|
62
|
+
- tenant_id: Tenant identifier for multi-tenancy
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Dict with success, outputs, execution_trace, and errors
|
|
66
|
+
"""
|
|
67
|
+
nodes = workflow_data.get("nodes", [])
|
|
68
|
+
edges = workflow_data.get("edges", [])
|
|
69
|
+
session_id = workflow_data.get("session_id", "default")
|
|
70
|
+
workflow_id = workflow_data.get("workflow_id")
|
|
71
|
+
tenant_id = workflow_data.get("tenant_id")
|
|
72
|
+
|
|
73
|
+
workflow.logger.info(
|
|
74
|
+
f"Starting workflow orchestration: {len(nodes)} nodes, {len(edges)} edges"
|
|
75
|
+
)
|
|
76
|
+
# Debug print for console visibility
|
|
77
|
+
print(f"[Workflow] Starting: {len(nodes)} nodes, {len(edges)} edges")
|
|
78
|
+
|
|
79
|
+
if not nodes:
|
|
80
|
+
return {
|
|
81
|
+
"success": False,
|
|
82
|
+
"error": "No nodes provided",
|
|
83
|
+
"outputs": {},
|
|
84
|
+
"execution_trace": [],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# 1. Filter out config nodes (tools, memory, services)
|
|
88
|
+
exec_nodes, exec_edges = self._filter_executable_graph(nodes, edges)
|
|
89
|
+
|
|
90
|
+
workflow.logger.info(
|
|
91
|
+
f"After filtering: {len(exec_nodes)} executable nodes "
|
|
92
|
+
f"(filtered {len(nodes) - len(exec_nodes)} config nodes)"
|
|
93
|
+
)
|
|
94
|
+
print(f"[Workflow] Executable: {len(exec_nodes)}, Config filtered: {len(nodes) - len(exec_nodes)}")
|
|
95
|
+
|
|
96
|
+
# 2. Build dependency maps
|
|
97
|
+
deps, node_map = self._build_dependency_maps(exec_nodes, exec_edges)
|
|
98
|
+
|
|
99
|
+
# 3. Initialize state
|
|
100
|
+
outputs: Dict[str, Any] = {} # node_id -> result
|
|
101
|
+
completed: Set[str] = set()
|
|
102
|
+
running: Dict[str, Any] = {} # node_id -> activity handle
|
|
103
|
+
errors: List[Dict] = []
|
|
104
|
+
execution_trace: List[str] = []
|
|
105
|
+
|
|
106
|
+
# 4. Handle pre-executed triggers (already have their output)
|
|
107
|
+
pre_executed_count = 0
|
|
108
|
+
for node in exec_nodes:
|
|
109
|
+
if node.get("_pre_executed"):
|
|
110
|
+
node_id = node["id"]
|
|
111
|
+
outputs[node_id] = {
|
|
112
|
+
"success": True,
|
|
113
|
+
"result": node.get("_trigger_output", {}),
|
|
114
|
+
"pre_executed": True,
|
|
115
|
+
}
|
|
116
|
+
completed.add(node_id)
|
|
117
|
+
execution_trace.append(node_id)
|
|
118
|
+
pre_executed_count += 1
|
|
119
|
+
workflow.logger.info(f"Pre-executed trigger: {node_id}")
|
|
120
|
+
|
|
121
|
+
workflow.logger.info(f"Pre-executed: {pre_executed_count}, To execute: {len(node_map) - pre_executed_count}")
|
|
122
|
+
print(f"[Workflow] Pre-executed: {pre_executed_count}, To execute: {len(node_map) - pre_executed_count}")
|
|
123
|
+
|
|
124
|
+
# 5. Retry policy for node activities
|
|
125
|
+
retry_policy = RetryPolicy(
|
|
126
|
+
initial_interval=timedelta(seconds=1),
|
|
127
|
+
maximum_interval=timedelta(seconds=30),
|
|
128
|
+
maximum_attempts=3,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# 6. Continuous scheduling loop
|
|
132
|
+
loop_count = 0
|
|
133
|
+
while True:
|
|
134
|
+
loop_count += 1
|
|
135
|
+
# Find ready nodes (all deps completed, not running/completed)
|
|
136
|
+
ready = self._find_ready_nodes(deps, completed, running, node_map)
|
|
137
|
+
workflow.logger.info(f"Loop {loop_count}: ready={len(ready)}, running={len(running)}, completed={len(completed)}")
|
|
138
|
+
print(f"[Workflow] Loop {loop_count}: ready={len(ready)}, running={len(running)}, completed={len(completed)}")
|
|
139
|
+
|
|
140
|
+
# Start activities for ready nodes
|
|
141
|
+
for node_id in ready:
|
|
142
|
+
node = node_map[node_id]
|
|
143
|
+
|
|
144
|
+
# Build immutable context for this node
|
|
145
|
+
context = {
|
|
146
|
+
"node_id": node_id,
|
|
147
|
+
"node_type": node.get("type", "unknown"),
|
|
148
|
+
"node_data": node.get("data", {}),
|
|
149
|
+
"inputs": self._get_node_inputs(node_id, deps, outputs),
|
|
150
|
+
"workflow_id": workflow_id,
|
|
151
|
+
"tenant_id": tenant_id,
|
|
152
|
+
"session_id": session_id,
|
|
153
|
+
"nodes": nodes, # Full list for tool/memory detection
|
|
154
|
+
"edges": edges, # Full list for tool/memory detection
|
|
155
|
+
# Include pre-executed info if applicable
|
|
156
|
+
"pre_executed": node.get("_pre_executed", False),
|
|
157
|
+
"trigger_output": node.get("_trigger_output"),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Schedule activity by name (for class-based activities)
|
|
161
|
+
# The activity is registered as NodeExecutionActivities.execute_node_activity
|
|
162
|
+
handle = workflow.start_activity(
|
|
163
|
+
"execute_node_activity",
|
|
164
|
+
args=[context],
|
|
165
|
+
start_to_close_timeout=timedelta(minutes=10),
|
|
166
|
+
heartbeat_timeout=timedelta(minutes=2),
|
|
167
|
+
retry_policy=retry_policy,
|
|
168
|
+
)
|
|
169
|
+
running[node_id] = handle
|
|
170
|
+
|
|
171
|
+
workflow.logger.info(f"Scheduled activity for node: {node_id}")
|
|
172
|
+
print(f"[Workflow] Scheduled: {node_id}")
|
|
173
|
+
|
|
174
|
+
# Exit if nothing running and nothing ready
|
|
175
|
+
if not running:
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
# Wait for ANY activity to complete (FIRST_COMPLETED pattern)
|
|
179
|
+
done_id, result = await self._wait_any_complete(running)
|
|
180
|
+
|
|
181
|
+
if result.get("success"):
|
|
182
|
+
outputs[done_id] = result
|
|
183
|
+
completed.add(done_id)
|
|
184
|
+
execution_trace.append(done_id)
|
|
185
|
+
workflow.logger.info(f"Node completed: {done_id}")
|
|
186
|
+
else:
|
|
187
|
+
# Node failed after all retries
|
|
188
|
+
error_info = {
|
|
189
|
+
"node_id": done_id,
|
|
190
|
+
"error": result.get("error", "Unknown error"),
|
|
191
|
+
}
|
|
192
|
+
errors.append(error_info)
|
|
193
|
+
workflow.logger.error(f"Node failed: {done_id} - {error_info['error']}")
|
|
194
|
+
|
|
195
|
+
# Stop workflow on failure
|
|
196
|
+
# TODO: Could add option to continue with partial results
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
# Build final result
|
|
200
|
+
success = len(errors) == 0 and len(completed) == len(node_map)
|
|
201
|
+
|
|
202
|
+
workflow.logger.info(
|
|
203
|
+
f"Workflow complete: success={success}, "
|
|
204
|
+
f"executed={len(execution_trace)}/{len(node_map)}"
|
|
205
|
+
)
|
|
206
|
+
print(f"[Workflow] Complete: success={success}, executed={len(execution_trace)}/{len(node_map)}")
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
"success": success,
|
|
210
|
+
"outputs": outputs,
|
|
211
|
+
"execution_trace": execution_trace,
|
|
212
|
+
"errors": errors if errors else None,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
def _get_node_inputs(
|
|
216
|
+
self,
|
|
217
|
+
node_id: str,
|
|
218
|
+
deps: Dict[str, Set[str]],
|
|
219
|
+
outputs: Dict[str, Any],
|
|
220
|
+
) -> Dict[str, Any]:
|
|
221
|
+
"""Get outputs from upstream nodes as inputs for this node."""
|
|
222
|
+
inputs = {}
|
|
223
|
+
for dep_id in deps.get(node_id, set()):
|
|
224
|
+
if dep_id in outputs:
|
|
225
|
+
inputs[dep_id] = outputs[dep_id].get("result", {})
|
|
226
|
+
return inputs
|
|
227
|
+
|
|
228
|
+
async def _wait_any_complete(self, running: Dict[str, Any]) -> tuple:
|
|
229
|
+
"""Wait for any activity to complete, return (node_id, result).
|
|
230
|
+
|
|
231
|
+
Uses Temporal's native wait mechanism for efficient polling.
|
|
232
|
+
"""
|
|
233
|
+
# Convert to list for iteration
|
|
234
|
+
items = list(running.items())
|
|
235
|
+
|
|
236
|
+
# Check if any already done
|
|
237
|
+
for node_id, handle in items:
|
|
238
|
+
if handle.done():
|
|
239
|
+
del running[node_id]
|
|
240
|
+
try:
|
|
241
|
+
result = await handle
|
|
242
|
+
return node_id, result
|
|
243
|
+
except Exception as e:
|
|
244
|
+
return node_id, {"success": False, "error": str(e)}
|
|
245
|
+
|
|
246
|
+
# Wait for first completion using Temporal's wait
|
|
247
|
+
handles = [h for _, h in items]
|
|
248
|
+
|
|
249
|
+
# Use asyncio.wait pattern via workflow.wait
|
|
250
|
+
await workflow.wait_condition(
|
|
251
|
+
lambda: any(h.done() for _, h in items)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Find the completed one
|
|
255
|
+
for node_id, handle in items:
|
|
256
|
+
if handle.done():
|
|
257
|
+
del running[node_id]
|
|
258
|
+
try:
|
|
259
|
+
result = await handle
|
|
260
|
+
return node_id, result
|
|
261
|
+
except Exception as e:
|
|
262
|
+
return node_id, {"success": False, "error": str(e)}
|
|
263
|
+
|
|
264
|
+
# Should not reach here
|
|
265
|
+
raise RuntimeError("No activity completed after wait")
|
|
266
|
+
|
|
267
|
+
def _filter_executable_graph(
|
|
268
|
+
self,
|
|
269
|
+
nodes: List[Dict],
|
|
270
|
+
edges: List[Dict],
|
|
271
|
+
) -> tuple:
|
|
272
|
+
"""Filter out config nodes based on edge handles.
|
|
273
|
+
|
|
274
|
+
Config nodes (tools, memory, model configs) connect via special handles
|
|
275
|
+
and are consumed by their target nodes, not executed independently.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Tuple of (executable_nodes, executable_edges)
|
|
279
|
+
"""
|
|
280
|
+
node_map = {n["id"]: n for n in nodes}
|
|
281
|
+
config_ids = set()
|
|
282
|
+
|
|
283
|
+
for edge in edges:
|
|
284
|
+
handle = edge.get("targetHandle", "")
|
|
285
|
+
source_id = edge.get("source")
|
|
286
|
+
|
|
287
|
+
# Edges to config handles mean source is a config node
|
|
288
|
+
if handle in CONFIG_HANDLES:
|
|
289
|
+
config_ids.add(source_id)
|
|
290
|
+
|
|
291
|
+
# Android services connecting to androidTool
|
|
292
|
+
source_node = node_map.get(source_id, {})
|
|
293
|
+
if source_node.get("type") in ANDROID_SERVICE_TYPES:
|
|
294
|
+
config_ids.add(source_id)
|
|
295
|
+
|
|
296
|
+
# Skill nodes (always config, connect to Chat Agent)
|
|
297
|
+
if source_node.get("type") in SKILL_NODE_TYPES:
|
|
298
|
+
config_ids.add(source_id)
|
|
299
|
+
|
|
300
|
+
# Filter nodes and edges
|
|
301
|
+
exec_nodes = [n for n in nodes if n["id"] not in config_ids]
|
|
302
|
+
exec_edges = [
|
|
303
|
+
e for e in edges
|
|
304
|
+
if e.get("source") not in config_ids
|
|
305
|
+
and e.get("target") not in config_ids
|
|
306
|
+
and e.get("targetHandle", "") not in CONFIG_HANDLES
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
return exec_nodes, exec_edges
|
|
310
|
+
|
|
311
|
+
def _build_dependency_maps(
|
|
312
|
+
self,
|
|
313
|
+
nodes: List[Dict],
|
|
314
|
+
edges: List[Dict],
|
|
315
|
+
) -> tuple:
|
|
316
|
+
"""Build dependency graph from nodes and edges.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Tuple of (dependencies_map, node_map)
|
|
320
|
+
- dependencies_map: node_id -> set of node IDs it depends on
|
|
321
|
+
- node_map: node_id -> node definition
|
|
322
|
+
"""
|
|
323
|
+
node_map = {n["id"]: n for n in nodes}
|
|
324
|
+
node_ids = set(node_map.keys())
|
|
325
|
+
|
|
326
|
+
deps = {nid: set() for nid in node_ids}
|
|
327
|
+
|
|
328
|
+
for edge in edges:
|
|
329
|
+
src, tgt = edge.get("source"), edge.get("target")
|
|
330
|
+
if src in node_ids and tgt in node_ids:
|
|
331
|
+
deps[tgt].add(src)
|
|
332
|
+
|
|
333
|
+
return deps, node_map
|
|
334
|
+
|
|
335
|
+
def _find_ready_nodes(
|
|
336
|
+
self,
|
|
337
|
+
deps: Dict[str, Set[str]],
|
|
338
|
+
completed: Set[str],
|
|
339
|
+
running: Dict[str, Any],
|
|
340
|
+
node_map: Dict[str, Dict],
|
|
341
|
+
) -> List[str]:
|
|
342
|
+
"""Find nodes ready to execute.
|
|
343
|
+
|
|
344
|
+
A node is ready when:
|
|
345
|
+
- All its dependencies have completed
|
|
346
|
+
- It's not already running
|
|
347
|
+
- It's not already completed
|
|
348
|
+
"""
|
|
349
|
+
ready = []
|
|
350
|
+
for node_id in node_map:
|
|
351
|
+
if node_id in completed or node_id in running:
|
|
352
|
+
continue
|
|
353
|
+
if deps[node_id] <= completed:
|
|
354
|
+
ready.append(node_id)
|
|
355
|
+
return ready
|