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,373 @@
|
|
|
1
|
+
"""Condition evaluation for runtime conditional branching.
|
|
2
|
+
|
|
3
|
+
Evaluates edge conditions against node outputs to determine
|
|
4
|
+
which paths to follow in a workflow (Prefect-style dynamic branching).
|
|
5
|
+
|
|
6
|
+
Supported operators:
|
|
7
|
+
- eq: Equal (==)
|
|
8
|
+
- neq: Not equal (!=)
|
|
9
|
+
- gt: Greater than (>)
|
|
10
|
+
- lt: Less than (<)
|
|
11
|
+
- gte: Greater than or equal (>=)
|
|
12
|
+
- lte: Less than or equal (<=)
|
|
13
|
+
- contains: String/list contains value
|
|
14
|
+
- not_contains: String/list does not contain value
|
|
15
|
+
- exists: Field exists and is not None
|
|
16
|
+
- not_exists: Field does not exist or is None
|
|
17
|
+
- is_empty: Field is empty (None, "", [], {})
|
|
18
|
+
- is_not_empty: Field is not empty
|
|
19
|
+
- matches: Regex pattern match
|
|
20
|
+
- in: Value is in list
|
|
21
|
+
- not_in: Value is not in list
|
|
22
|
+
- starts_with: String starts with value
|
|
23
|
+
- ends_with: String ends with value
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from typing import Dict, Any, Optional, List, Union
|
|
28
|
+
|
|
29
|
+
from core.logging import get_logger
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Type alias for condition dict
|
|
35
|
+
ConditionDict = Dict[str, Any]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_nested_value(data: Dict[str, Any], field_path: str) -> Any:
|
|
39
|
+
"""Get a nested value from a dictionary using dot notation.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
data: Dictionary to extract value from
|
|
43
|
+
field_path: Dot-separated path (e.g., "result.status", "items.0.name")
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Value at path or None if not found
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
>>> get_nested_value({"result": {"status": "ok"}}, "result.status")
|
|
50
|
+
"ok"
|
|
51
|
+
>>> get_nested_value({"items": [{"name": "a"}]}, "items.0.name")
|
|
52
|
+
"a"
|
|
53
|
+
"""
|
|
54
|
+
if not data or not field_path:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
parts = field_path.split('.')
|
|
58
|
+
current = data
|
|
59
|
+
|
|
60
|
+
for part in parts:
|
|
61
|
+
if current is None:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# Handle array index
|
|
65
|
+
if part.isdigit():
|
|
66
|
+
index = int(part)
|
|
67
|
+
if isinstance(current, (list, tuple)) and 0 <= index < len(current):
|
|
68
|
+
current = current[index]
|
|
69
|
+
else:
|
|
70
|
+
return None
|
|
71
|
+
# Handle dict key
|
|
72
|
+
elif isinstance(current, dict):
|
|
73
|
+
current = current.get(part)
|
|
74
|
+
else:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
return current
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def evaluate_condition(condition: ConditionDict, output: Dict[str, Any]) -> bool:
|
|
81
|
+
"""Evaluate an edge condition against node output.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
condition: Condition dict with field, operator, value
|
|
85
|
+
{
|
|
86
|
+
"field": "status", # Output field to check
|
|
87
|
+
"operator": "eq", # Comparison operator
|
|
88
|
+
"value": "success" # Value to compare against
|
|
89
|
+
}
|
|
90
|
+
output: Node execution output dict
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if condition matches, False otherwise
|
|
94
|
+
"""
|
|
95
|
+
if not condition:
|
|
96
|
+
return True # No condition = always follow
|
|
97
|
+
|
|
98
|
+
field = condition.get("field", "")
|
|
99
|
+
operator = condition.get("operator", "eq")
|
|
100
|
+
target_value = condition.get("value")
|
|
101
|
+
|
|
102
|
+
# Get the actual value from output
|
|
103
|
+
actual_value = get_nested_value(output, field)
|
|
104
|
+
|
|
105
|
+
logger.debug("Evaluating condition",
|
|
106
|
+
field=field,
|
|
107
|
+
operator=operator,
|
|
108
|
+
target=target_value,
|
|
109
|
+
actual=actual_value)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
result = _evaluate_operator(operator, actual_value, target_value)
|
|
113
|
+
logger.debug("Condition result", result=result)
|
|
114
|
+
return result
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.warning("Condition evaluation error",
|
|
117
|
+
field=field,
|
|
118
|
+
operator=operator,
|
|
119
|
+
error=str(e))
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _evaluate_operator(operator: str, actual: Any, target: Any) -> bool:
|
|
124
|
+
"""Evaluate a single operator.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
operator: Operator name
|
|
128
|
+
actual: Actual value from output
|
|
129
|
+
target: Target value to compare
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Comparison result
|
|
133
|
+
"""
|
|
134
|
+
# Equality operators
|
|
135
|
+
if operator == "eq":
|
|
136
|
+
return actual == target
|
|
137
|
+
|
|
138
|
+
elif operator == "neq":
|
|
139
|
+
return actual != target
|
|
140
|
+
|
|
141
|
+
# Comparison operators (numeric)
|
|
142
|
+
elif operator == "gt":
|
|
143
|
+
return _safe_compare(actual, target, lambda a, b: a > b)
|
|
144
|
+
|
|
145
|
+
elif operator == "lt":
|
|
146
|
+
return _safe_compare(actual, target, lambda a, b: a < b)
|
|
147
|
+
|
|
148
|
+
elif operator == "gte":
|
|
149
|
+
return _safe_compare(actual, target, lambda a, b: a >= b)
|
|
150
|
+
|
|
151
|
+
elif operator == "lte":
|
|
152
|
+
return _safe_compare(actual, target, lambda a, b: a <= b)
|
|
153
|
+
|
|
154
|
+
# String/list contains
|
|
155
|
+
elif operator == "contains":
|
|
156
|
+
if actual is None:
|
|
157
|
+
return False
|
|
158
|
+
if isinstance(actual, str):
|
|
159
|
+
return str(target) in actual
|
|
160
|
+
elif isinstance(actual, (list, tuple)):
|
|
161
|
+
return target in actual
|
|
162
|
+
elif isinstance(actual, dict):
|
|
163
|
+
return target in actual
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
elif operator == "not_contains":
|
|
167
|
+
return not _evaluate_operator("contains", actual, target)
|
|
168
|
+
|
|
169
|
+
# Existence checks
|
|
170
|
+
elif operator == "exists":
|
|
171
|
+
return actual is not None
|
|
172
|
+
|
|
173
|
+
elif operator == "not_exists":
|
|
174
|
+
return actual is None
|
|
175
|
+
|
|
176
|
+
# Empty checks
|
|
177
|
+
elif operator == "is_empty":
|
|
178
|
+
if actual is None:
|
|
179
|
+
return True
|
|
180
|
+
if isinstance(actual, (str, list, dict, tuple)):
|
|
181
|
+
return len(actual) == 0
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
elif operator == "is_not_empty":
|
|
185
|
+
return not _evaluate_operator("is_empty", actual, target)
|
|
186
|
+
|
|
187
|
+
# Regex match
|
|
188
|
+
elif operator == "matches":
|
|
189
|
+
if actual is None or target is None:
|
|
190
|
+
return False
|
|
191
|
+
try:
|
|
192
|
+
return bool(re.search(str(target), str(actual)))
|
|
193
|
+
except re.error:
|
|
194
|
+
logger.warning("Invalid regex pattern", pattern=target)
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
# List membership
|
|
198
|
+
elif operator == "in":
|
|
199
|
+
if not isinstance(target, (list, tuple)):
|
|
200
|
+
return actual == target
|
|
201
|
+
return actual in target
|
|
202
|
+
|
|
203
|
+
elif operator == "not_in":
|
|
204
|
+
return not _evaluate_operator("in", actual, target)
|
|
205
|
+
|
|
206
|
+
# String prefix/suffix
|
|
207
|
+
elif operator == "starts_with":
|
|
208
|
+
if actual is None or target is None:
|
|
209
|
+
return False
|
|
210
|
+
return str(actual).startswith(str(target))
|
|
211
|
+
|
|
212
|
+
elif operator == "ends_with":
|
|
213
|
+
if actual is None or target is None:
|
|
214
|
+
return False
|
|
215
|
+
return str(actual).endswith(str(target))
|
|
216
|
+
|
|
217
|
+
# Boolean checks
|
|
218
|
+
elif operator == "is_true":
|
|
219
|
+
return actual is True or actual == "true" or actual == 1
|
|
220
|
+
|
|
221
|
+
elif operator == "is_false":
|
|
222
|
+
return actual is False or actual == "false" or actual == 0
|
|
223
|
+
|
|
224
|
+
# Type checks
|
|
225
|
+
elif operator == "is_string":
|
|
226
|
+
return isinstance(actual, str)
|
|
227
|
+
|
|
228
|
+
elif operator == "is_number":
|
|
229
|
+
return isinstance(actual, (int, float)) and not isinstance(actual, bool)
|
|
230
|
+
|
|
231
|
+
elif operator == "is_boolean":
|
|
232
|
+
return isinstance(actual, bool)
|
|
233
|
+
|
|
234
|
+
elif operator == "is_array":
|
|
235
|
+
return isinstance(actual, (list, tuple))
|
|
236
|
+
|
|
237
|
+
elif operator == "is_object":
|
|
238
|
+
return isinstance(actual, dict)
|
|
239
|
+
|
|
240
|
+
else:
|
|
241
|
+
logger.warning("Unknown operator", operator=operator)
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _safe_compare(actual: Any, target: Any, comparator) -> bool:
|
|
246
|
+
"""Safely compare two values, handling type coercion.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
actual: Actual value
|
|
250
|
+
target: Target value
|
|
251
|
+
comparator: Comparison function
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Comparison result, False if comparison impossible
|
|
255
|
+
"""
|
|
256
|
+
if actual is None or target is None:
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
# Try numeric comparison first
|
|
260
|
+
try:
|
|
261
|
+
return comparator(float(actual), float(target))
|
|
262
|
+
except (ValueError, TypeError):
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
# Fall back to string comparison
|
|
266
|
+
try:
|
|
267
|
+
return comparator(str(actual), str(target))
|
|
268
|
+
except (ValueError, TypeError):
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def evaluate_conditions(conditions: List[ConditionDict], output: Dict[str, Any],
|
|
273
|
+
logic: str = "and") -> bool:
|
|
274
|
+
"""Evaluate multiple conditions with AND/OR logic.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
conditions: List of condition dicts
|
|
278
|
+
output: Node execution output
|
|
279
|
+
logic: "and" (all must match) or "or" (any must match)
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Combined evaluation result
|
|
283
|
+
"""
|
|
284
|
+
if not conditions:
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
results = [evaluate_condition(c, output) for c in conditions]
|
|
288
|
+
|
|
289
|
+
if logic == "or":
|
|
290
|
+
return any(results)
|
|
291
|
+
else: # "and"
|
|
292
|
+
return all(results)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def decide_next_edges(edges: List[Dict[str, Any]], source_node_id: str,
|
|
296
|
+
output: Dict[str, Any]) -> List[str]:
|
|
297
|
+
"""Determine which edges to follow based on conditions.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
edges: All edges in workflow
|
|
301
|
+
source_node_id: ID of the node that just completed
|
|
302
|
+
output: Output from the completed node
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
List of target node IDs to execute next
|
|
306
|
+
"""
|
|
307
|
+
next_nodes = []
|
|
308
|
+
unconditional_edges = []
|
|
309
|
+
conditional_edges = []
|
|
310
|
+
|
|
311
|
+
# Separate edges by type
|
|
312
|
+
for edge in edges:
|
|
313
|
+
if edge.get("source") != source_node_id:
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
condition = edge.get("data", {}).get("condition")
|
|
317
|
+
if condition:
|
|
318
|
+
conditional_edges.append(edge)
|
|
319
|
+
else:
|
|
320
|
+
unconditional_edges.append(edge)
|
|
321
|
+
|
|
322
|
+
# If there are conditional edges, evaluate them
|
|
323
|
+
if conditional_edges:
|
|
324
|
+
for edge in conditional_edges:
|
|
325
|
+
condition = edge.get("data", {}).get("condition")
|
|
326
|
+
if evaluate_condition(condition, output):
|
|
327
|
+
next_nodes.append(edge["target"])
|
|
328
|
+
logger.info("Conditional edge matched",
|
|
329
|
+
source=source_node_id,
|
|
330
|
+
target=edge["target"],
|
|
331
|
+
condition=condition)
|
|
332
|
+
|
|
333
|
+
# If no conditional edges matched, fall through to unconditional
|
|
334
|
+
if not next_nodes and unconditional_edges:
|
|
335
|
+
logger.info("No conditional edges matched, using unconditional",
|
|
336
|
+
source=source_node_id)
|
|
337
|
+
for edge in unconditional_edges:
|
|
338
|
+
next_nodes.append(edge["target"])
|
|
339
|
+
else:
|
|
340
|
+
# No conditions - follow all unconditional edges
|
|
341
|
+
for edge in unconditional_edges:
|
|
342
|
+
next_nodes.append(edge["target"])
|
|
343
|
+
|
|
344
|
+
return next_nodes
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# Operator metadata for frontend UI
|
|
348
|
+
OPERATORS = {
|
|
349
|
+
"eq": {"label": "Equals", "description": "Value equals target", "requires_value": True},
|
|
350
|
+
"neq": {"label": "Not Equals", "description": "Value does not equal target", "requires_value": True},
|
|
351
|
+
"gt": {"label": "Greater Than", "description": "Value is greater than target", "requires_value": True},
|
|
352
|
+
"lt": {"label": "Less Than", "description": "Value is less than target", "requires_value": True},
|
|
353
|
+
"gte": {"label": "Greater or Equal", "description": "Value is greater than or equal to target", "requires_value": True},
|
|
354
|
+
"lte": {"label": "Less or Equal", "description": "Value is less than or equal to target", "requires_value": True},
|
|
355
|
+
"contains": {"label": "Contains", "description": "String/list contains value", "requires_value": True},
|
|
356
|
+
"not_contains": {"label": "Does Not Contain", "description": "String/list does not contain value", "requires_value": True},
|
|
357
|
+
"exists": {"label": "Exists", "description": "Field exists and is not null", "requires_value": False},
|
|
358
|
+
"not_exists": {"label": "Does Not Exist", "description": "Field does not exist or is null", "requires_value": False},
|
|
359
|
+
"is_empty": {"label": "Is Empty", "description": "Value is empty (null, '', [], {})", "requires_value": False},
|
|
360
|
+
"is_not_empty": {"label": "Is Not Empty", "description": "Value is not empty", "requires_value": False},
|
|
361
|
+
"matches": {"label": "Matches Regex", "description": "Value matches regex pattern", "requires_value": True},
|
|
362
|
+
"in": {"label": "In List", "description": "Value is in list", "requires_value": True},
|
|
363
|
+
"not_in": {"label": "Not In List", "description": "Value is not in list", "requires_value": True},
|
|
364
|
+
"starts_with": {"label": "Starts With", "description": "String starts with value", "requires_value": True},
|
|
365
|
+
"ends_with": {"label": "Ends With", "description": "String ends with value", "requires_value": True},
|
|
366
|
+
"is_true": {"label": "Is True", "description": "Value is truthy", "requires_value": False},
|
|
367
|
+
"is_false": {"label": "Is False", "description": "Value is falsy", "requires_value": False},
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def get_available_operators() -> Dict[str, Dict[str, Any]]:
|
|
372
|
+
"""Get operator metadata for frontend UI."""
|
|
373
|
+
return OPERATORS.copy()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Dead Letter Queue (DLQ) handler for failed node executions.
|
|
2
|
+
|
|
3
|
+
This module provides optional DLQ functionality that can be enabled/disabled
|
|
4
|
+
via configuration. When enabled, failed nodes (after all retries exhausted)
|
|
5
|
+
are stored for later inspection and replay.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from services.execution.dlq import DLQHandler, NullDLQHandler
|
|
9
|
+
|
|
10
|
+
# Create handler based on config
|
|
11
|
+
dlq = DLQHandler(cache) if settings.dlq_enabled else NullDLQHandler()
|
|
12
|
+
|
|
13
|
+
# Add failed node
|
|
14
|
+
await dlq.add_failed_node(ctx, node, inputs, error)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Dict, Any, Protocol
|
|
18
|
+
from core.logging import get_logger
|
|
19
|
+
from .models import ExecutionContext, NodeExecution, DLQEntry
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DLQHandlerProtocol(Protocol):
|
|
25
|
+
"""Protocol for DLQ handlers (enables duck typing)."""
|
|
26
|
+
|
|
27
|
+
async def add_failed_node(self, ctx: ExecutionContext, node: NodeExecution,
|
|
28
|
+
inputs: Dict[str, Any], error: str) -> bool:
|
|
29
|
+
"""Add a failed node to the DLQ."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def enabled(self) -> bool:
|
|
34
|
+
"""Whether DLQ is enabled."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NullDLQHandler:
|
|
39
|
+
"""No-op DLQ handler when DLQ is disabled.
|
|
40
|
+
|
|
41
|
+
This follows the Null Object pattern - all operations succeed silently.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def enabled(self) -> bool:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
async def add_failed_node(self, ctx: ExecutionContext, node: NodeExecution,
|
|
49
|
+
inputs: Dict[str, Any], error: str) -> bool:
|
|
50
|
+
"""No-op: silently succeed without storing anything."""
|
|
51
|
+
logger.debug("DLQ disabled, skipping failed node storage",
|
|
52
|
+
node_id=node.node_id, error=error)
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DLQHandler:
|
|
57
|
+
"""Active DLQ handler that stores failed nodes in Redis.
|
|
58
|
+
|
|
59
|
+
Stores failed node executions with full context for later inspection
|
|
60
|
+
and replay via the replay_dlq_entry API.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, cache: "ExecutionCache"):
|
|
64
|
+
"""Initialize DLQ handler.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
cache: ExecutionCache instance for Redis persistence
|
|
68
|
+
"""
|
|
69
|
+
self.cache = cache
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def enabled(self) -> bool:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
async def add_failed_node(self, ctx: ExecutionContext, node: NodeExecution,
|
|
76
|
+
inputs: Dict[str, Any], error: str) -> bool:
|
|
77
|
+
"""Add a failed node to the Dead Letter Queue.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
ctx: ExecutionContext with workflow info
|
|
81
|
+
node: Failed NodeExecution with retry info
|
|
82
|
+
inputs: Node inputs at time of failure
|
|
83
|
+
error: Final error message
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if successfully added, False otherwise
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
dlq_entry = DLQEntry.create(ctx, node, inputs)
|
|
90
|
+
|
|
91
|
+
success = await self.cache.add_to_dlq(dlq_entry)
|
|
92
|
+
if success:
|
|
93
|
+
logger.info("Node added to DLQ",
|
|
94
|
+
entry_id=dlq_entry.id,
|
|
95
|
+
node_id=node.node_id,
|
|
96
|
+
node_type=node.node_type,
|
|
97
|
+
retry_count=node.retry_count)
|
|
98
|
+
|
|
99
|
+
await self.cache.add_event(ctx.execution_id, "node_dlq", {
|
|
100
|
+
"node_id": node.node_id,
|
|
101
|
+
"dlq_entry_id": dlq_entry.id,
|
|
102
|
+
"error": error,
|
|
103
|
+
"retry_count": node.retry_count,
|
|
104
|
+
})
|
|
105
|
+
return True
|
|
106
|
+
else:
|
|
107
|
+
logger.error("Failed to add node to DLQ",
|
|
108
|
+
node_id=node.node_id, error=error)
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error("Exception adding node to DLQ",
|
|
113
|
+
node_id=node.node_id, error=str(e))
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def create_dlq_handler(cache: "ExecutionCache", enabled: bool = False) -> DLQHandlerProtocol:
|
|
118
|
+
"""Factory function to create appropriate DLQ handler.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
cache: ExecutionCache instance
|
|
122
|
+
enabled: Whether DLQ should be enabled
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
DLQHandler if enabled, NullDLQHandler otherwise
|
|
126
|
+
"""
|
|
127
|
+
if enabled:
|
|
128
|
+
logger.info("DLQ enabled")
|
|
129
|
+
return DLQHandler(cache)
|
|
130
|
+
else:
|
|
131
|
+
logger.debug("DLQ disabled")
|
|
132
|
+
return NullDLQHandler()
|