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,531 @@
|
|
|
1
|
+
"""Execution engine state models.
|
|
2
|
+
|
|
3
|
+
Based on Netflix Conductor task lifecycle and Prefect 3.0 patterns.
|
|
4
|
+
All models are JSON-serializable for Redis persistence and cross-runtime portability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass, field, asdict
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Dict, Any, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaskStatus(str, Enum):
|
|
18
|
+
"""Task execution states (Conductor-style lifecycle).
|
|
19
|
+
|
|
20
|
+
State transitions:
|
|
21
|
+
PENDING -> SCHEDULED -> RUNNING -> COMPLETED
|
|
22
|
+
-> FAILED
|
|
23
|
+
-> CANCELLED
|
|
24
|
+
CACHED (Prefect pattern - result from cache, no execution)
|
|
25
|
+
"""
|
|
26
|
+
PENDING = "pending" # Created, not yet scheduled
|
|
27
|
+
SCHEDULED = "scheduled" # In task queue, waiting for worker
|
|
28
|
+
RUNNING = "running" # Worker executing
|
|
29
|
+
COMPLETED = "completed" # Success, result cached
|
|
30
|
+
FAILED = "failed" # Error, may retry
|
|
31
|
+
CACHED = "cached" # Result from cache (Prefect pattern)
|
|
32
|
+
CANCELLED = "cancelled" # User cancelled
|
|
33
|
+
WAITING = "waiting" # Waiting for external event (triggers)
|
|
34
|
+
SKIPPED = "skipped" # Skipped due to condition
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WorkflowStatus(str, Enum):
|
|
38
|
+
"""Workflow execution states."""
|
|
39
|
+
PENDING = "pending" # Created, not started
|
|
40
|
+
RUNNING = "running" # At least one node executing
|
|
41
|
+
PAUSED = "paused" # User paused
|
|
42
|
+
COMPLETED = "completed" # All nodes completed successfully
|
|
43
|
+
FAILED = "failed" # Execution failed
|
|
44
|
+
CANCELLED = "cancelled" # User cancelled
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class RetryPolicy:
|
|
49
|
+
"""Retry configuration for node execution.
|
|
50
|
+
|
|
51
|
+
Implements exponential backoff with configurable limits.
|
|
52
|
+
Delay formula: min(initial_delay * (backoff_multiplier ^ attempt), max_delay)
|
|
53
|
+
"""
|
|
54
|
+
max_attempts: int = 3
|
|
55
|
+
initial_delay: float = 1.0 # seconds
|
|
56
|
+
max_delay: float = 60.0 # seconds
|
|
57
|
+
backoff_multiplier: float = 2.0
|
|
58
|
+
retry_on_timeout: bool = True
|
|
59
|
+
retry_on_connection_error: bool = True
|
|
60
|
+
retry_on_server_error: bool = True # 5xx errors
|
|
61
|
+
|
|
62
|
+
def calculate_delay(self, attempt: int) -> float:
|
|
63
|
+
"""Calculate delay before next retry attempt.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
attempt: Current attempt number (0-indexed)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Delay in seconds before next attempt
|
|
70
|
+
"""
|
|
71
|
+
delay = self.initial_delay * (self.backoff_multiplier ** attempt)
|
|
72
|
+
return min(delay, self.max_delay)
|
|
73
|
+
|
|
74
|
+
def should_retry(self, error: str, attempt: int) -> bool:
|
|
75
|
+
"""Determine if execution should be retried.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
error: Error message from failed execution
|
|
79
|
+
attempt: Current attempt number (0-indexed)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if should retry, False otherwise
|
|
83
|
+
"""
|
|
84
|
+
if attempt >= self.max_attempts:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
error_lower = error.lower()
|
|
88
|
+
|
|
89
|
+
if self.retry_on_timeout and "timeout" in error_lower:
|
|
90
|
+
return True
|
|
91
|
+
if self.retry_on_connection_error and ("connection" in error_lower or "connect" in error_lower):
|
|
92
|
+
return True
|
|
93
|
+
if self.retry_on_server_error and ("500" in error or "502" in error or "503" in error or "504" in error):
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
99
|
+
"""Convert to JSON-serializable dict."""
|
|
100
|
+
return {
|
|
101
|
+
"max_attempts": self.max_attempts,
|
|
102
|
+
"initial_delay": self.initial_delay,
|
|
103
|
+
"max_delay": self.max_delay,
|
|
104
|
+
"backoff_multiplier": self.backoff_multiplier,
|
|
105
|
+
"retry_on_timeout": self.retry_on_timeout,
|
|
106
|
+
"retry_on_connection_error": self.retry_on_connection_error,
|
|
107
|
+
"retry_on_server_error": self.retry_on_server_error,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_dict(cls, data: Dict[str, Any]) -> "RetryPolicy":
|
|
112
|
+
"""Create from dict."""
|
|
113
|
+
return cls(
|
|
114
|
+
max_attempts=data.get("max_attempts", 3),
|
|
115
|
+
initial_delay=data.get("initial_delay", 1.0),
|
|
116
|
+
max_delay=data.get("max_delay", 60.0),
|
|
117
|
+
backoff_multiplier=data.get("backoff_multiplier", 2.0),
|
|
118
|
+
retry_on_timeout=data.get("retry_on_timeout", True),
|
|
119
|
+
retry_on_connection_error=data.get("retry_on_connection_error", True),
|
|
120
|
+
retry_on_server_error=data.get("retry_on_server_error", True),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Default retry policies for different node types
|
|
125
|
+
DEFAULT_RETRY_POLICIES: Dict[str, RetryPolicy] = {
|
|
126
|
+
"httpRequest": RetryPolicy(max_attempts=3, initial_delay=2.0),
|
|
127
|
+
"webhookTrigger": RetryPolicy(max_attempts=1), # Don't retry triggers
|
|
128
|
+
"whatsappReceive": RetryPolicy(max_attempts=1), # Don't retry triggers
|
|
129
|
+
"aiAgent": RetryPolicy(max_attempts=2, initial_delay=5.0, max_delay=30.0),
|
|
130
|
+
"openaiChatModel": RetryPolicy(max_attempts=2, initial_delay=5.0),
|
|
131
|
+
"anthropicChatModel": RetryPolicy(max_attempts=2, initial_delay=5.0),
|
|
132
|
+
"googleChatModel": RetryPolicy(max_attempts=2, initial_delay=5.0),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_retry_policy(node_type: str, custom_policy: Dict = None) -> RetryPolicy:
|
|
137
|
+
"""Get retry policy for a node type.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
node_type: The node type string
|
|
141
|
+
custom_policy: Optional custom policy dict from node parameters
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
RetryPolicy instance
|
|
145
|
+
"""
|
|
146
|
+
if custom_policy:
|
|
147
|
+
return RetryPolicy.from_dict(custom_policy)
|
|
148
|
+
return DEFAULT_RETRY_POLICIES.get(node_type, RetryPolicy())
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class DLQEntry:
|
|
153
|
+
"""Dead Letter Queue entry for failed node executions.
|
|
154
|
+
|
|
155
|
+
Stores failed execution details for manual review and replay.
|
|
156
|
+
"""
|
|
157
|
+
id: str
|
|
158
|
+
execution_id: str
|
|
159
|
+
workflow_id: str
|
|
160
|
+
node_id: str
|
|
161
|
+
node_type: str
|
|
162
|
+
error: str
|
|
163
|
+
inputs: Dict[str, Any]
|
|
164
|
+
retry_count: int
|
|
165
|
+
created_at: float = field(default_factory=time.time)
|
|
166
|
+
last_error_at: float = field(default_factory=time.time)
|
|
167
|
+
|
|
168
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
169
|
+
"""Convert to JSON-serializable dict."""
|
|
170
|
+
return {
|
|
171
|
+
"id": self.id,
|
|
172
|
+
"execution_id": self.execution_id,
|
|
173
|
+
"workflow_id": self.workflow_id,
|
|
174
|
+
"node_id": self.node_id,
|
|
175
|
+
"node_type": self.node_type,
|
|
176
|
+
"error": self.error,
|
|
177
|
+
"inputs": self.inputs,
|
|
178
|
+
"retry_count": self.retry_count,
|
|
179
|
+
"created_at": self.created_at,
|
|
180
|
+
"last_error_at": self.last_error_at,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def from_dict(cls, data: Dict[str, Any]) -> "DLQEntry":
|
|
185
|
+
"""Create from dict."""
|
|
186
|
+
return cls(
|
|
187
|
+
id=data["id"],
|
|
188
|
+
execution_id=data["execution_id"],
|
|
189
|
+
workflow_id=data["workflow_id"],
|
|
190
|
+
node_id=data["node_id"],
|
|
191
|
+
node_type=data["node_type"],
|
|
192
|
+
error=data["error"],
|
|
193
|
+
inputs=data.get("inputs", {}),
|
|
194
|
+
retry_count=data.get("retry_count", 0),
|
|
195
|
+
created_at=data.get("created_at", time.time()),
|
|
196
|
+
last_error_at=data.get("last_error_at", time.time()),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def create(cls, ctx: "ExecutionContext", node_exec: "NodeExecution",
|
|
201
|
+
inputs: Dict[str, Any]) -> "DLQEntry":
|
|
202
|
+
"""Factory method to create DLQ entry from failed execution."""
|
|
203
|
+
return cls(
|
|
204
|
+
id=str(uuid.uuid4()),
|
|
205
|
+
execution_id=ctx.execution_id,
|
|
206
|
+
workflow_id=ctx.workflow_id,
|
|
207
|
+
node_id=node_exec.node_id,
|
|
208
|
+
node_type=node_exec.node_type,
|
|
209
|
+
error=node_exec.error or "Unknown error",
|
|
210
|
+
inputs=inputs,
|
|
211
|
+
retry_count=node_exec.retry_count,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class NodeExecution:
|
|
217
|
+
"""Tracks execution state for a single node.
|
|
218
|
+
|
|
219
|
+
Prefect-style: includes input hash for cache lookup.
|
|
220
|
+
"""
|
|
221
|
+
node_id: str
|
|
222
|
+
node_type: str
|
|
223
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
224
|
+
input_hash: Optional[str] = None # For cache lookup
|
|
225
|
+
output: Optional[Dict[str, Any]] = None
|
|
226
|
+
error: Optional[str] = None
|
|
227
|
+
started_at: Optional[float] = None
|
|
228
|
+
completed_at: Optional[float] = None
|
|
229
|
+
retry_count: int = 0
|
|
230
|
+
|
|
231
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
232
|
+
"""Convert to JSON-serializable dict."""
|
|
233
|
+
return {
|
|
234
|
+
"node_id": self.node_id,
|
|
235
|
+
"node_type": self.node_type,
|
|
236
|
+
"status": self.status.value,
|
|
237
|
+
"input_hash": self.input_hash,
|
|
238
|
+
"output": self.output,
|
|
239
|
+
"error": self.error,
|
|
240
|
+
"started_at": self.started_at,
|
|
241
|
+
"completed_at": self.completed_at,
|
|
242
|
+
"retry_count": self.retry_count,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def from_dict(cls, data: Dict[str, Any]) -> "NodeExecution":
|
|
247
|
+
"""Create from dict (Redis deserialization)."""
|
|
248
|
+
return cls(
|
|
249
|
+
node_id=data["node_id"],
|
|
250
|
+
node_type=data["node_type"],
|
|
251
|
+
status=TaskStatus(data["status"]),
|
|
252
|
+
input_hash=data.get("input_hash"),
|
|
253
|
+
output=data.get("output"),
|
|
254
|
+
error=data.get("error"),
|
|
255
|
+
started_at=data.get("started_at"),
|
|
256
|
+
completed_at=data.get("completed_at"),
|
|
257
|
+
retry_count=data.get("retry_count", 0),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass
|
|
262
|
+
class ExecutionContext:
|
|
263
|
+
"""Isolated execution context for a workflow run.
|
|
264
|
+
|
|
265
|
+
Replaces global _deployment_running flag.
|
|
266
|
+
Each workflow execution gets its own context with isolated state.
|
|
267
|
+
|
|
268
|
+
Conductor pattern: workflow_id identifies the workflow definition,
|
|
269
|
+
execution_id identifies this specific run.
|
|
270
|
+
"""
|
|
271
|
+
execution_id: str
|
|
272
|
+
workflow_id: str
|
|
273
|
+
status: WorkflowStatus = WorkflowStatus.PENDING
|
|
274
|
+
session_id: str = "default"
|
|
275
|
+
|
|
276
|
+
# Node states and outputs
|
|
277
|
+
node_executions: Dict[str, NodeExecution] = field(default_factory=dict)
|
|
278
|
+
outputs: Dict[str, Any] = field(default_factory=dict)
|
|
279
|
+
|
|
280
|
+
# DAG structure (cached for parallel batch detection)
|
|
281
|
+
nodes: List[Dict[str, Any]] = field(default_factory=list)
|
|
282
|
+
edges: List[Dict[str, Any]] = field(default_factory=list)
|
|
283
|
+
|
|
284
|
+
# Execution tracking
|
|
285
|
+
execution_order: List[str] = field(default_factory=list)
|
|
286
|
+
current_layer: int = 0
|
|
287
|
+
checkpoints: List[str] = field(default_factory=list)
|
|
288
|
+
|
|
289
|
+
# Timing
|
|
290
|
+
created_at: float = field(default_factory=time.time)
|
|
291
|
+
updated_at: float = field(default_factory=time.time)
|
|
292
|
+
started_at: Optional[float] = None
|
|
293
|
+
completed_at: Optional[float] = None
|
|
294
|
+
|
|
295
|
+
# Error tracking
|
|
296
|
+
errors: List[Dict[str, Any]] = field(default_factory=list)
|
|
297
|
+
|
|
298
|
+
@classmethod
|
|
299
|
+
def create(cls, workflow_id: str, session_id: str = "default",
|
|
300
|
+
nodes: List[Dict] = None, edges: List[Dict] = None) -> "ExecutionContext":
|
|
301
|
+
"""Factory method to create new execution context.
|
|
302
|
+
|
|
303
|
+
Supports pre-executed nodes (marked with _pre_executed=True) for
|
|
304
|
+
event-driven execution where trigger nodes are already complete.
|
|
305
|
+
|
|
306
|
+
Config nodes (memory, tools, model configs) are excluded from execution
|
|
307
|
+
as they provide configuration to other nodes via special handles.
|
|
308
|
+
|
|
309
|
+
Toolkit sub-nodes (nodes connected TO a toolkit like androidTool) are also
|
|
310
|
+
excluded - they execute only when called via the toolkit's tool interface.
|
|
311
|
+
"""
|
|
312
|
+
from constants import CONFIG_NODE_TYPES, TOOLKIT_NODE_TYPES
|
|
313
|
+
|
|
314
|
+
execution_id = str(uuid.uuid4())
|
|
315
|
+
ctx = cls(
|
|
316
|
+
execution_id=execution_id,
|
|
317
|
+
workflow_id=workflow_id,
|
|
318
|
+
session_id=session_id,
|
|
319
|
+
nodes=nodes or [],
|
|
320
|
+
edges=edges or [],
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Find toolkit sub-nodes (nodes that connect TO a toolkit node)
|
|
324
|
+
# These should only execute when called via the toolkit, not as workflow nodes
|
|
325
|
+
toolkit_node_ids = {n.get("id") for n in (nodes or []) if n.get("type") in TOOLKIT_NODE_TYPES}
|
|
326
|
+
|
|
327
|
+
# Find AI Agent nodes (both aiAgent and chatAgent have config handles)
|
|
328
|
+
ai_agent_node_ids = {n.get("id") for n in (nodes or []) if n.get("type") in ('aiAgent', 'chatAgent')}
|
|
329
|
+
|
|
330
|
+
subnode_ids: set = set()
|
|
331
|
+
for edge in (edges or []):
|
|
332
|
+
source = edge.get("source")
|
|
333
|
+
target = edge.get("target")
|
|
334
|
+
target_handle = edge.get("targetHandle")
|
|
335
|
+
|
|
336
|
+
# Any node that connects TO a toolkit is a sub-node
|
|
337
|
+
if target in toolkit_node_ids and source:
|
|
338
|
+
subnode_ids.add(source)
|
|
339
|
+
|
|
340
|
+
# Nodes connected to AI Agent/Chat Agent config handles are sub-nodes
|
|
341
|
+
# These handles: input-memory, input-tools, input-skill
|
|
342
|
+
if target in ai_agent_node_ids and source and target_handle:
|
|
343
|
+
if target_handle in ('input-memory', 'input-tools', 'input-skill'):
|
|
344
|
+
subnode_ids.add(source)
|
|
345
|
+
|
|
346
|
+
# Initialize node executions for all nodes (excluding config nodes and sub-nodes)
|
|
347
|
+
for node in (nodes or []):
|
|
348
|
+
node_id = node.get("id")
|
|
349
|
+
node_type = node.get("type", "unknown")
|
|
350
|
+
|
|
351
|
+
# Skip config nodes - they don't execute independently
|
|
352
|
+
# They provide configuration to other nodes via special handles
|
|
353
|
+
if node_type in CONFIG_NODE_TYPES:
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
# Skip toolkit sub-nodes - they execute only via toolkit tool calls
|
|
357
|
+
if node_id in subnode_ids:
|
|
358
|
+
continue
|
|
359
|
+
|
|
360
|
+
# Check if node is pre-executed (e.g., trigger that already fired)
|
|
361
|
+
if node.get("_pre_executed"):
|
|
362
|
+
# Mark as COMPLETED with trigger output
|
|
363
|
+
trigger_output = node.get("_trigger_output", {})
|
|
364
|
+
node_exec = NodeExecution(
|
|
365
|
+
node_id=node_id,
|
|
366
|
+
node_type=node_type,
|
|
367
|
+
status=TaskStatus.COMPLETED,
|
|
368
|
+
output=trigger_output,
|
|
369
|
+
completed_at=time.time(),
|
|
370
|
+
)
|
|
371
|
+
ctx.outputs[node_id] = trigger_output
|
|
372
|
+
ctx.checkpoints.append(node_id)
|
|
373
|
+
else:
|
|
374
|
+
node_exec = NodeExecution(
|
|
375
|
+
node_id=node_id,
|
|
376
|
+
node_type=node_type,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
ctx.node_executions[node_id] = node_exec
|
|
380
|
+
|
|
381
|
+
return ctx
|
|
382
|
+
|
|
383
|
+
def get_node_status(self, node_id: str) -> Optional[TaskStatus]:
|
|
384
|
+
"""Get status for a specific node."""
|
|
385
|
+
node_exec = self.node_executions.get(node_id)
|
|
386
|
+
return node_exec.status if node_exec else None
|
|
387
|
+
|
|
388
|
+
def set_node_status(self, node_id: str, status: TaskStatus,
|
|
389
|
+
output: Dict = None, error: str = None) -> None:
|
|
390
|
+
"""Update node execution status."""
|
|
391
|
+
if node_id not in self.node_executions:
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
node_exec = self.node_executions[node_id]
|
|
395
|
+
node_exec.status = status
|
|
396
|
+
self.updated_at = time.time()
|
|
397
|
+
|
|
398
|
+
if status == TaskStatus.RUNNING:
|
|
399
|
+
node_exec.started_at = time.time()
|
|
400
|
+
elif status in (TaskStatus.COMPLETED, TaskStatus.CACHED):
|
|
401
|
+
node_exec.completed_at = time.time()
|
|
402
|
+
if output:
|
|
403
|
+
node_exec.output = output
|
|
404
|
+
self.outputs[node_id] = output
|
|
405
|
+
elif status == TaskStatus.SKIPPED:
|
|
406
|
+
# Skipped due to conditional branching - mark as completed but no output
|
|
407
|
+
node_exec.completed_at = time.time()
|
|
408
|
+
elif status == TaskStatus.FAILED:
|
|
409
|
+
node_exec.completed_at = time.time()
|
|
410
|
+
if error:
|
|
411
|
+
node_exec.error = error
|
|
412
|
+
self.errors.append({
|
|
413
|
+
"node_id": node_id,
|
|
414
|
+
"error": error,
|
|
415
|
+
"timestamp": time.time()
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
def add_checkpoint(self, node_id: str) -> None:
|
|
419
|
+
"""Add checkpoint after node completion (for recovery)."""
|
|
420
|
+
self.checkpoints.append(node_id)
|
|
421
|
+
self.updated_at = time.time()
|
|
422
|
+
|
|
423
|
+
def get_completed_nodes(self) -> List[str]:
|
|
424
|
+
"""Get list of completed node IDs."""
|
|
425
|
+
return [
|
|
426
|
+
node_id for node_id, node_exec in self.node_executions.items()
|
|
427
|
+
if node_exec.status in (TaskStatus.COMPLETED, TaskStatus.CACHED, TaskStatus.SKIPPED)
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
def get_pending_nodes(self) -> List[str]:
|
|
431
|
+
"""Get list of pending node IDs."""
|
|
432
|
+
return [
|
|
433
|
+
node_id for node_id, node_exec in self.node_executions.items()
|
|
434
|
+
if node_exec.status == TaskStatus.PENDING
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
def all_nodes_complete(self) -> bool:
|
|
438
|
+
"""Check if all nodes are complete."""
|
|
439
|
+
for node_exec in self.node_executions.values():
|
|
440
|
+
if node_exec.status not in (TaskStatus.COMPLETED, TaskStatus.CACHED,
|
|
441
|
+
TaskStatus.SKIPPED, TaskStatus.CANCELLED):
|
|
442
|
+
return False
|
|
443
|
+
return True
|
|
444
|
+
|
|
445
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
446
|
+
"""Convert to JSON-serializable dict for Redis storage."""
|
|
447
|
+
return {
|
|
448
|
+
"execution_id": self.execution_id,
|
|
449
|
+
"workflow_id": self.workflow_id,
|
|
450
|
+
"status": self.status.value,
|
|
451
|
+
"session_id": self.session_id,
|
|
452
|
+
"node_executions": {
|
|
453
|
+
k: v.to_dict() for k, v in self.node_executions.items()
|
|
454
|
+
},
|
|
455
|
+
"outputs": self.outputs,
|
|
456
|
+
"execution_order": self.execution_order,
|
|
457
|
+
"current_layer": self.current_layer,
|
|
458
|
+
"checkpoints": self.checkpoints,
|
|
459
|
+
"created_at": self.created_at,
|
|
460
|
+
"updated_at": self.updated_at,
|
|
461
|
+
"started_at": self.started_at,
|
|
462
|
+
"completed_at": self.completed_at,
|
|
463
|
+
"errors": self.errors,
|
|
464
|
+
# Don't store full nodes/edges - too large
|
|
465
|
+
"node_count": len(self.nodes),
|
|
466
|
+
"edge_count": len(self.edges),
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
@classmethod
|
|
470
|
+
def from_dict(cls, data: Dict[str, Any], nodes: List[Dict] = None,
|
|
471
|
+
edges: List[Dict] = None) -> "ExecutionContext":
|
|
472
|
+
"""Create from dict (Redis deserialization)."""
|
|
473
|
+
ctx = cls(
|
|
474
|
+
execution_id=data["execution_id"],
|
|
475
|
+
workflow_id=data["workflow_id"],
|
|
476
|
+
status=WorkflowStatus(data["status"]),
|
|
477
|
+
session_id=data.get("session_id", "default"),
|
|
478
|
+
nodes=nodes or [],
|
|
479
|
+
edges=edges or [],
|
|
480
|
+
execution_order=data.get("execution_order", []),
|
|
481
|
+
current_layer=data.get("current_layer", 0),
|
|
482
|
+
checkpoints=data.get("checkpoints", []),
|
|
483
|
+
created_at=data.get("created_at", time.time()),
|
|
484
|
+
updated_at=data.get("updated_at", time.time()),
|
|
485
|
+
started_at=data.get("started_at"),
|
|
486
|
+
completed_at=data.get("completed_at"),
|
|
487
|
+
errors=data.get("errors", []),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Restore node executions
|
|
491
|
+
for node_id, node_data in data.get("node_executions", {}).items():
|
|
492
|
+
ctx.node_executions[node_id] = NodeExecution.from_dict(node_data)
|
|
493
|
+
|
|
494
|
+
# Restore outputs
|
|
495
|
+
ctx.outputs = data.get("outputs", {})
|
|
496
|
+
|
|
497
|
+
return ctx
|
|
498
|
+
|
|
499
|
+
def to_json(self) -> str:
|
|
500
|
+
"""Serialize to JSON string."""
|
|
501
|
+
return json.dumps(self.to_dict())
|
|
502
|
+
|
|
503
|
+
@classmethod
|
|
504
|
+
def from_json(cls, json_str: str, nodes: List[Dict] = None,
|
|
505
|
+
edges: List[Dict] = None) -> "ExecutionContext":
|
|
506
|
+
"""Deserialize from JSON string."""
|
|
507
|
+
data = json.loads(json_str)
|
|
508
|
+
return cls.from_dict(data, nodes, edges)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def hash_inputs(inputs: Dict[str, Any]) -> str:
|
|
512
|
+
"""Generate deterministic hash of inputs for cache key (Prefect pattern).
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
inputs: Dictionary of input parameters
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
SHA256 hash of canonicalized inputs
|
|
519
|
+
"""
|
|
520
|
+
# Canonical JSON (sorted keys, no extra whitespace)
|
|
521
|
+
canonical = json.dumps(inputs, sort_keys=True, separators=(",", ":"))
|
|
522
|
+
return hashlib.sha256(canonical.encode()).hexdigest()[:16]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def generate_cache_key(execution_id: str, node_id: str, inputs: Dict[str, Any]) -> str:
|
|
526
|
+
"""Generate cache key for node result (Prefect pattern).
|
|
527
|
+
|
|
528
|
+
Format: result:{execution_id}:{node_id}:{input_hash}
|
|
529
|
+
"""
|
|
530
|
+
input_hash = hash_inputs(inputs)
|
|
531
|
+
return f"result:{execution_id}:{node_id}:{input_hash}"
|