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,375 @@
|
|
|
1
|
+
"""Node Executor - Single node execution with handler dispatch.
|
|
2
|
+
|
|
3
|
+
Uses a registry pattern for clean handler dispatch without if-else chains.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from functools import partial
|
|
12
|
+
from typing import Dict, Any, Optional, Callable, TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from core.logging import get_logger
|
|
15
|
+
from constants import (
|
|
16
|
+
ANDROID_SERVICE_NODE_TYPES,
|
|
17
|
+
AI_MODEL_TYPES,
|
|
18
|
+
AI_CHAT_MODEL_TYPES,
|
|
19
|
+
GOOGLE_MAPS_TYPES,
|
|
20
|
+
detect_ai_provider,
|
|
21
|
+
)
|
|
22
|
+
from models.nodes import validate_node_params
|
|
23
|
+
from pydantic import ValidationError
|
|
24
|
+
from services import event_waiter
|
|
25
|
+
from services.handlers import (
|
|
26
|
+
handle_ai_agent, handle_chat_agent, handle_ai_chat_model, handle_simple_memory,
|
|
27
|
+
handle_android_device_setup, handle_android_service,
|
|
28
|
+
handle_python_executor, handle_javascript_executor,
|
|
29
|
+
handle_http_request, handle_webhook_response, handle_trigger_node,
|
|
30
|
+
handle_create_map, handle_add_locations, handle_nearby_places,
|
|
31
|
+
handle_text_generator, handle_file_handler,
|
|
32
|
+
handle_chat_send, handle_chat_history,
|
|
33
|
+
handle_start, handle_cron_scheduler, handle_timer, handle_console,
|
|
34
|
+
handle_whatsapp_send, handle_whatsapp_connect, handle_whatsapp_db,
|
|
35
|
+
handle_http_scraper, handle_file_downloader, handle_document_parser,
|
|
36
|
+
handle_text_chunker, handle_embedding_generator, handle_vector_store,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from core.config import Settings
|
|
41
|
+
from core.database import Database
|
|
42
|
+
from services.ai import AIService
|
|
43
|
+
from services.maps import MapsService
|
|
44
|
+
from services.text import TextService
|
|
45
|
+
from services.android_service import AndroidService
|
|
46
|
+
|
|
47
|
+
logger = get_logger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ExecutionResult:
|
|
52
|
+
"""Standardized execution result."""
|
|
53
|
+
success: bool
|
|
54
|
+
node_id: str
|
|
55
|
+
node_type: str
|
|
56
|
+
result: Optional[Dict] = None
|
|
57
|
+
error: Optional[str] = None
|
|
58
|
+
execution_id: str = ""
|
|
59
|
+
execution_time: float = 0.0
|
|
60
|
+
timestamp: str = ""
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
63
|
+
d = {
|
|
64
|
+
"success": self.success,
|
|
65
|
+
"node_id": self.node_id,
|
|
66
|
+
"node_type": self.node_type,
|
|
67
|
+
"execution_id": self.execution_id,
|
|
68
|
+
"execution_time": self.execution_time,
|
|
69
|
+
"timestamp": self.timestamp or datetime.now().isoformat(),
|
|
70
|
+
}
|
|
71
|
+
if self.success:
|
|
72
|
+
d["result"] = self.result or {}
|
|
73
|
+
else:
|
|
74
|
+
d["error"] = self.error
|
|
75
|
+
return d
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class NodeExecutor:
|
|
79
|
+
"""Executes individual workflow nodes using registry-based dispatch."""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
database: "Database",
|
|
84
|
+
ai_service: "AIService",
|
|
85
|
+
maps_service: "MapsService",
|
|
86
|
+
text_service: "TextService",
|
|
87
|
+
android_service: "AndroidService",
|
|
88
|
+
settings: "Settings",
|
|
89
|
+
output_store: Optional[Callable] = None,
|
|
90
|
+
):
|
|
91
|
+
self.database = database
|
|
92
|
+
self.ai_service = ai_service
|
|
93
|
+
self.maps_service = maps_service
|
|
94
|
+
self.text_service = text_service
|
|
95
|
+
self.android_service = android_service
|
|
96
|
+
self.settings = settings
|
|
97
|
+
self._output_store = output_store
|
|
98
|
+
self._handlers = self._build_handler_registry()
|
|
99
|
+
|
|
100
|
+
def _build_handler_registry(self) -> Dict[str, Callable]:
|
|
101
|
+
"""Build handler registry with service dependencies bound via partial."""
|
|
102
|
+
registry = {
|
|
103
|
+
# Workflow control
|
|
104
|
+
'start': handle_start,
|
|
105
|
+
'cronScheduler': handle_cron_scheduler,
|
|
106
|
+
'timer': handle_timer,
|
|
107
|
+
# AI
|
|
108
|
+
'aiAgent': partial(handle_ai_agent, ai_service=self.ai_service, database=self.database),
|
|
109
|
+
'chatAgent': partial(handle_chat_agent, ai_service=self.ai_service, database=self.database),
|
|
110
|
+
'simpleMemory': handle_simple_memory,
|
|
111
|
+
# Maps
|
|
112
|
+
'createMap': partial(handle_create_map, maps_service=self.maps_service),
|
|
113
|
+
'addLocations': partial(handle_add_locations, maps_service=self.maps_service),
|
|
114
|
+
'showNearbyPlaces': partial(handle_nearby_places, maps_service=self.maps_service),
|
|
115
|
+
# Text
|
|
116
|
+
'textGenerator': partial(handle_text_generator, text_service=self.text_service),
|
|
117
|
+
'fileHandler': partial(handle_file_handler, text_service=self.text_service),
|
|
118
|
+
# WhatsApp
|
|
119
|
+
'whatsappSend': handle_whatsapp_send,
|
|
120
|
+
'whatsappConnect': handle_whatsapp_connect,
|
|
121
|
+
'whatsappDb': handle_whatsapp_db,
|
|
122
|
+
# Chat
|
|
123
|
+
'chatSend': handle_chat_send,
|
|
124
|
+
'chatHistory': handle_chat_history,
|
|
125
|
+
# HTTP
|
|
126
|
+
'httpRequest': handle_http_request,
|
|
127
|
+
# Android setup
|
|
128
|
+
'androidDeviceSetup': partial(handle_android_device_setup, settings=self.settings),
|
|
129
|
+
# Document processing
|
|
130
|
+
'httpScraper': handle_http_scraper,
|
|
131
|
+
'fileDownloader': handle_file_downloader,
|
|
132
|
+
'documentParser': handle_document_parser,
|
|
133
|
+
'textChunker': handle_text_chunker,
|
|
134
|
+
'embeddingGenerator': handle_embedding_generator,
|
|
135
|
+
'vectorStore': handle_vector_store,
|
|
136
|
+
# Note: 'console' handled in _dispatch with connected_outputs
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Register AI chat models
|
|
140
|
+
for node_type in AI_CHAT_MODEL_TYPES:
|
|
141
|
+
registry[node_type] = partial(handle_ai_chat_model, ai_service=self.ai_service)
|
|
142
|
+
|
|
143
|
+
# Register Android services
|
|
144
|
+
for node_type in ANDROID_SERVICE_NODE_TYPES:
|
|
145
|
+
registry[node_type] = partial(handle_android_service, android_service=self.android_service)
|
|
146
|
+
|
|
147
|
+
return registry
|
|
148
|
+
|
|
149
|
+
async def execute(
|
|
150
|
+
self,
|
|
151
|
+
node_id: str,
|
|
152
|
+
node_type: str,
|
|
153
|
+
parameters: Dict[str, Any],
|
|
154
|
+
context: Dict[str, Any],
|
|
155
|
+
resolve_params_fn: Optional[Callable] = None,
|
|
156
|
+
) -> Dict[str, Any]:
|
|
157
|
+
"""Execute a single workflow node."""
|
|
158
|
+
start_time = time.time()
|
|
159
|
+
session_id = context.get('session_id', 'default')
|
|
160
|
+
execution_id = context.get('execution_id') or str(uuid.uuid4())[:8]
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
# Load, validate, enhance parameters
|
|
164
|
+
params = await self._prepare_parameters(node_id, node_type, parameters, session_id)
|
|
165
|
+
|
|
166
|
+
# Resolve templates if resolver provided
|
|
167
|
+
nodes = context.get('nodes')
|
|
168
|
+
edges = context.get('edges')
|
|
169
|
+
logger.debug(f"[NodeExecutor] Template resolution check: resolve_fn={resolve_params_fn is not None}, nodes={len(nodes) if nodes else 'None'}, edges={len(edges) if edges else 'None'}")
|
|
170
|
+
|
|
171
|
+
if resolve_params_fn and nodes is not None and edges is not None:
|
|
172
|
+
logger.debug(f"[NodeExecutor] Before resolution: params={list(params.keys())}")
|
|
173
|
+
params = await resolve_params_fn(params, node_id, nodes, edges, session_id)
|
|
174
|
+
logger.debug(f"[NodeExecutor] After resolution: params keys={list(params.keys())}")
|
|
175
|
+
|
|
176
|
+
# Build handler context
|
|
177
|
+
handler_ctx = {
|
|
178
|
+
**context,
|
|
179
|
+
"start_time": start_time,
|
|
180
|
+
"execution_id": execution_id,
|
|
181
|
+
}
|
|
182
|
+
logger.info("NodeExecutor context", node_id=node_id, workflow_id=context.get('workflow_id'))
|
|
183
|
+
|
|
184
|
+
# Execute via registry or special handlers
|
|
185
|
+
result = await self._dispatch(node_id, node_type, params, handler_ctx)
|
|
186
|
+
result['execution_id'] = execution_id
|
|
187
|
+
|
|
188
|
+
# Store output if successful
|
|
189
|
+
if result.get('success') and self._output_store:
|
|
190
|
+
output_data = result.get('result', {})
|
|
191
|
+
|
|
192
|
+
# For Android service nodes, extract the nested 'data' field for cleaner template access
|
|
193
|
+
# This allows {{batterymonitor.battery_level}} instead of {{batterymonitor.data.battery_level}}
|
|
194
|
+
if node_type in ANDROID_SERVICE_NODE_TYPES and isinstance(output_data, dict):
|
|
195
|
+
# Flatten: promote 'data' contents to top level while preserving metadata
|
|
196
|
+
nested_data = output_data.get('data', {})
|
|
197
|
+
if isinstance(nested_data, dict):
|
|
198
|
+
# Merge nested data with metadata (service_id, action, timestamp, etc.)
|
|
199
|
+
output_data = {**output_data, **nested_data}
|
|
200
|
+
logger.debug(f"[NodeExecutor] Flattened Android output for {node_id}: keys={list(output_data.keys())}")
|
|
201
|
+
|
|
202
|
+
await self._output_store(session_id, node_id, "output_0", output_data)
|
|
203
|
+
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
except asyncio.CancelledError:
|
|
207
|
+
return ExecutionResult(False, node_id, node_type, error="Cancelled",
|
|
208
|
+
execution_id=execution_id, execution_time=time.time()-start_time).to_dict()
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error("Node execution error", node_id=node_id, error=str(e))
|
|
211
|
+
return ExecutionResult(False, node_id, node_type, error=str(e),
|
|
212
|
+
execution_id=execution_id, execution_time=time.time()-start_time).to_dict()
|
|
213
|
+
|
|
214
|
+
async def _prepare_parameters(self, node_id: str, node_type: str, params: Dict, session_id: str) -> Dict:
|
|
215
|
+
"""Load from DB, validate, inject API keys."""
|
|
216
|
+
# Merge with DB parameters (DB provides defaults, frontend can override)
|
|
217
|
+
db_params = await self.database.get_node_parameters(node_id) or {}
|
|
218
|
+
merged = {**db_params, **params} if params else db_params
|
|
219
|
+
|
|
220
|
+
# Validate
|
|
221
|
+
try:
|
|
222
|
+
validated = validate_node_params(node_type, merged)
|
|
223
|
+
merged = {**merged, **validated.model_dump(by_alias=True, exclude_unset=True)}
|
|
224
|
+
except ValidationError as e:
|
|
225
|
+
logger.warning("Validation warning", node_type=node_type, errors=str(e))
|
|
226
|
+
|
|
227
|
+
# Inject API keys
|
|
228
|
+
return await self._inject_api_keys(node_type, merged)
|
|
229
|
+
|
|
230
|
+
async def _inject_api_keys(self, node_type: str, params: Dict) -> Dict:
|
|
231
|
+
"""Auto-inject API keys for AI and Maps nodes."""
|
|
232
|
+
result = params.copy()
|
|
233
|
+
|
|
234
|
+
if node_type in AI_MODEL_TYPES:
|
|
235
|
+
provider = detect_ai_provider(node_type, params)
|
|
236
|
+
if not result.get('api_key') and not result.get('apiKey'):
|
|
237
|
+
key = await self.ai_service.auth.get_api_key(provider, "default")
|
|
238
|
+
if key:
|
|
239
|
+
result['api_key'] = key
|
|
240
|
+
if not result.get('model'):
|
|
241
|
+
models = await self.ai_service.auth.get_stored_models(provider, "default")
|
|
242
|
+
if models:
|
|
243
|
+
result['model'] = models[0]
|
|
244
|
+
|
|
245
|
+
elif node_type in GOOGLE_MAPS_TYPES:
|
|
246
|
+
if not result.get('api_key'):
|
|
247
|
+
# Try database first, then fall back to environment variable
|
|
248
|
+
key = await self.ai_service.auth.get_api_key("google_maps", "default")
|
|
249
|
+
if key:
|
|
250
|
+
result['api_key'] = key
|
|
251
|
+
elif self.settings.google_maps_api_key:
|
|
252
|
+
result['api_key'] = self.settings.google_maps_api_key
|
|
253
|
+
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
async def _dispatch(self, node_id: str, node_type: str, params: Dict, context: Dict) -> Dict:
|
|
257
|
+
"""Dispatch to handler from registry or special handlers."""
|
|
258
|
+
|
|
259
|
+
# Check registry first
|
|
260
|
+
handler = self._handlers.get(node_type)
|
|
261
|
+
if handler:
|
|
262
|
+
return await handler(node_id, node_type, params, context)
|
|
263
|
+
|
|
264
|
+
# Special handlers needing connected outputs
|
|
265
|
+
if node_type in ('pythonExecutor', 'javascriptExecutor', 'webhookResponse', 'console'):
|
|
266
|
+
outputs, source_nodes = await self._get_connected_outputs_with_info(context, node_id)
|
|
267
|
+
if node_type == 'console':
|
|
268
|
+
return await handle_console(node_id, node_type, params, context, outputs, source_nodes)
|
|
269
|
+
handlers = {
|
|
270
|
+
'pythonExecutor': handle_python_executor,
|
|
271
|
+
'javascriptExecutor': handle_javascript_executor,
|
|
272
|
+
'webhookResponse': handle_webhook_response,
|
|
273
|
+
}
|
|
274
|
+
return await handlers[node_type](node_id, node_type, params, context, outputs)
|
|
275
|
+
|
|
276
|
+
# Trigger nodes
|
|
277
|
+
if event_waiter.is_trigger_node(node_type):
|
|
278
|
+
return await handle_trigger_node(node_id, node_type, params, context)
|
|
279
|
+
|
|
280
|
+
# Fallback
|
|
281
|
+
return {
|
|
282
|
+
"success": True,
|
|
283
|
+
"node_id": node_id,
|
|
284
|
+
"node_type": node_type,
|
|
285
|
+
"result": {"message": f"Node {node_id} executed", "parameters": params},
|
|
286
|
+
"execution_time": time.time() - context.get('start_time', time.time()),
|
|
287
|
+
"timestamp": datetime.now().isoformat()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async def _get_connected_outputs(self, context: Dict, node_id: str) -> Dict[str, Any]:
|
|
291
|
+
"""Get outputs from connected upstream nodes."""
|
|
292
|
+
get_output = context.get('get_output_fn')
|
|
293
|
+
if not get_output:
|
|
294
|
+
return {}
|
|
295
|
+
|
|
296
|
+
nodes = context.get('nodes', [])
|
|
297
|
+
edges = context.get('edges', [])
|
|
298
|
+
session_id = context.get('session_id', 'default')
|
|
299
|
+
result = {}
|
|
300
|
+
|
|
301
|
+
for edge in edges:
|
|
302
|
+
if edge.get('target') == node_id:
|
|
303
|
+
source_id = edge.get('source')
|
|
304
|
+
output = await get_output(session_id, source_id, "output_0")
|
|
305
|
+
if output:
|
|
306
|
+
source = next((n for n in nodes if n.get('id') == source_id), {})
|
|
307
|
+
result[source.get('type', 'unknown')] = output
|
|
308
|
+
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
def _get_source_nodes_info(self, context: Dict, node_id: str) -> list:
|
|
312
|
+
"""Get source node info (id, type, label) for edges targeting this node.
|
|
313
|
+
|
|
314
|
+
This is used for display purposes (e.g., showing source in Console panel).
|
|
315
|
+
Does NOT filter by output availability - just returns edge source info.
|
|
316
|
+
"""
|
|
317
|
+
nodes = context.get('nodes', [])
|
|
318
|
+
edges = context.get('edges', [])
|
|
319
|
+
source_nodes = []
|
|
320
|
+
|
|
321
|
+
for edge in edges:
|
|
322
|
+
if edge.get('target') == node_id:
|
|
323
|
+
source_id = edge.get('source')
|
|
324
|
+
source = next((n for n in nodes if n.get('id') == source_id), {})
|
|
325
|
+
source_type = source.get('type', 'unknown')
|
|
326
|
+
source_data = source.get('data', {})
|
|
327
|
+
source_label = source_data.get('label') or source_type
|
|
328
|
+
source_nodes.append({
|
|
329
|
+
'id': source_id,
|
|
330
|
+
'type': source_type,
|
|
331
|
+
'label': source_label
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
return source_nodes
|
|
335
|
+
|
|
336
|
+
async def _get_connected_outputs_with_info(self, context: Dict, node_id: str) -> tuple:
|
|
337
|
+
"""Get outputs from connected upstream nodes with source node info.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Tuple of (outputs dict, source_nodes list with id/type/label info)
|
|
341
|
+
"""
|
|
342
|
+
get_output = context.get('get_output_fn')
|
|
343
|
+
if not get_output:
|
|
344
|
+
logger.warning(f"[_get_connected_outputs_with_info] No get_output_fn in context for {node_id}")
|
|
345
|
+
return {}, []
|
|
346
|
+
|
|
347
|
+
nodes = context.get('nodes', [])
|
|
348
|
+
edges = context.get('edges', [])
|
|
349
|
+
session_id = context.get('session_id', 'default')
|
|
350
|
+
outputs = {}
|
|
351
|
+
source_nodes = []
|
|
352
|
+
|
|
353
|
+
logger.debug(f"[_get_connected_outputs_with_info] node_id={node_id}, edges={len(edges)}, session={session_id}")
|
|
354
|
+
|
|
355
|
+
for edge in edges:
|
|
356
|
+
if edge.get('target') == node_id:
|
|
357
|
+
source_id = edge.get('source')
|
|
358
|
+
logger.debug(f"[_get_connected_outputs_with_info] Found edge from {source_id} to {node_id}")
|
|
359
|
+
output = await get_output(session_id, source_id, "output_0")
|
|
360
|
+
logger.debug(f"[_get_connected_outputs_with_info] Output from {source_id}: {'FOUND' if output else 'NOT FOUND'}")
|
|
361
|
+
if output:
|
|
362
|
+
source = next((n for n in nodes if n.get('id') == source_id), {})
|
|
363
|
+
source_type = source.get('type', 'unknown')
|
|
364
|
+
outputs[source_type] = output
|
|
365
|
+
# Get label from node data if available
|
|
366
|
+
source_data = source.get('data', {})
|
|
367
|
+
source_label = source_data.get('label') or source_type
|
|
368
|
+
source_nodes.append({
|
|
369
|
+
'id': source_id,
|
|
370
|
+
'type': source_type,
|
|
371
|
+
'label': source_label
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
logger.debug(f"[_get_connected_outputs_with_info] Returning {len(outputs)} outputs, {len(source_nodes)} source_nodes")
|
|
375
|
+
return outputs, source_nodes
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Parameter Resolver - Template variable resolution.
|
|
2
|
+
|
|
3
|
+
Resolves {{node.field}} template variables in parameters using connected node outputs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Dict, Any, List, Optional, Callable, TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from core.logging import get_logger
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from core.database import Database
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
# Compiled regex for template matching
|
|
17
|
+
TEMPLATE_PATTERN = re.compile(r'\{\{([^}]+)\}\}')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ParameterResolver:
|
|
21
|
+
"""Resolves template variables in node parameters."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, database: "Database", get_output_fn: Callable):
|
|
24
|
+
"""
|
|
25
|
+
Args:
|
|
26
|
+
database: Database for loading node parameters
|
|
27
|
+
get_output_fn: Async function to get node output
|
|
28
|
+
Signature: async def (session_id, node_id, output_name) -> Dict
|
|
29
|
+
"""
|
|
30
|
+
self.database = database
|
|
31
|
+
self.get_output = get_output_fn
|
|
32
|
+
|
|
33
|
+
async def resolve(
|
|
34
|
+
self,
|
|
35
|
+
parameters: Dict[str, Any],
|
|
36
|
+
node_id: str,
|
|
37
|
+
nodes: List[Dict],
|
|
38
|
+
edges: List[Dict],
|
|
39
|
+
session_id: str
|
|
40
|
+
) -> Dict[str, Any]:
|
|
41
|
+
"""Resolve all template variables in parameters."""
|
|
42
|
+
# Build connected data map from upstream nodes
|
|
43
|
+
connected_data = await self._gather_connected_outputs(node_id, nodes, edges, session_id)
|
|
44
|
+
|
|
45
|
+
# Resolve templates
|
|
46
|
+
return self._resolve_templates(parameters, connected_data)
|
|
47
|
+
|
|
48
|
+
async def _gather_connected_outputs(
|
|
49
|
+
self,
|
|
50
|
+
node_id: str,
|
|
51
|
+
nodes: List[Dict],
|
|
52
|
+
edges: List[Dict],
|
|
53
|
+
session_id: str
|
|
54
|
+
) -> Dict[str, Any]:
|
|
55
|
+
"""Gather outputs from all nodes in the workflow that have executed.
|
|
56
|
+
|
|
57
|
+
n8n pattern: Template variables can reference ANY node's output in the workflow,
|
|
58
|
+
not just directly connected nodes. This allows flexible data flow patterns like:
|
|
59
|
+
- A -> B -> C where C references A's output directly
|
|
60
|
+
- Parallel branches where downstream nodes reference any upstream node
|
|
61
|
+
"""
|
|
62
|
+
connected = {}
|
|
63
|
+
|
|
64
|
+
logger.debug(f"[ParameterResolver] Gathering outputs for node {node_id}, session_id={session_id}, total nodes: {len(nodes)}")
|
|
65
|
+
|
|
66
|
+
# Gather outputs from ALL nodes (not just directly connected)
|
|
67
|
+
# This allows {{nodeName.field}} to reference any previously executed node
|
|
68
|
+
for source_node in nodes:
|
|
69
|
+
source_id = source_node.get('id')
|
|
70
|
+
if source_id == node_id:
|
|
71
|
+
continue # Skip self
|
|
72
|
+
|
|
73
|
+
node_type = source_node.get('type', '')
|
|
74
|
+
node_label = source_node.get('data', {}).get('label', 'NO_LABEL')
|
|
75
|
+
node_key = self._get_template_key(source_node)
|
|
76
|
+
|
|
77
|
+
logger.debug(f"[ParameterResolver] Processing node: id={source_id}, type={node_type}, label={node_label}, key={node_key}")
|
|
78
|
+
|
|
79
|
+
# Special handling for start nodes
|
|
80
|
+
if node_type == 'start':
|
|
81
|
+
data = await self._get_start_node_data(source_id)
|
|
82
|
+
else:
|
|
83
|
+
data = await self.get_output(session_id, source_id, "output_0")
|
|
84
|
+
logger.debug(f"[ParameterResolver] Output lookup: session={session_id}, node={source_id}, result={'FOUND' if data else 'NOT_FOUND'}")
|
|
85
|
+
|
|
86
|
+
if data:
|
|
87
|
+
connected[node_key] = data
|
|
88
|
+
logger.debug(f"[ParameterResolver] Stored output for key '{node_key}' (type={node_type}): keys={list(data.keys()) if isinstance(data, dict) else type(data)}")
|
|
89
|
+
|
|
90
|
+
logger.debug(f"[ParameterResolver] Available data keys for resolution: {list(connected.keys())}")
|
|
91
|
+
return connected
|
|
92
|
+
|
|
93
|
+
async def _get_start_node_data(self, node_id: str) -> Optional[Dict]:
|
|
94
|
+
"""Get initial data from start node parameters."""
|
|
95
|
+
import json
|
|
96
|
+
params = await self.database.get_node_parameters(node_id)
|
|
97
|
+
if not params or 'initialData' not in params:
|
|
98
|
+
return {}
|
|
99
|
+
|
|
100
|
+
initial_data = params.get('initialData', '{}')
|
|
101
|
+
try:
|
|
102
|
+
return json.loads(initial_data) if isinstance(initial_data, str) else initial_data
|
|
103
|
+
except Exception:
|
|
104
|
+
return {}
|
|
105
|
+
|
|
106
|
+
def _get_template_key(self, node: Dict) -> str:
|
|
107
|
+
"""Get template key for a node (lowercase, no spaces).
|
|
108
|
+
|
|
109
|
+
Priority matches frontend useDragVariable hook:
|
|
110
|
+
1. node.data.label (user-defined label)
|
|
111
|
+
2. node.data.displayName (from node definition)
|
|
112
|
+
3. node.type (lowercased)
|
|
113
|
+
4. node.id (fallback)
|
|
114
|
+
"""
|
|
115
|
+
# Priority 1: User-defined label
|
|
116
|
+
label = node.get('data', {}).get('label')
|
|
117
|
+
if label:
|
|
118
|
+
return re.sub(r'\s+', '', label.lower())
|
|
119
|
+
|
|
120
|
+
# Priority 2: displayName from node definition (passed in node.data)
|
|
121
|
+
display_name = node.get('data', {}).get('displayName')
|
|
122
|
+
if display_name:
|
|
123
|
+
return re.sub(r'\s+', '', display_name.lower())
|
|
124
|
+
|
|
125
|
+
# Priority 3: node type
|
|
126
|
+
node_type = node.get('type', '')
|
|
127
|
+
if node_type:
|
|
128
|
+
return node_type.lower()
|
|
129
|
+
|
|
130
|
+
# Priority 4: node id
|
|
131
|
+
return node.get('id', 'unknown').lower()
|
|
132
|
+
|
|
133
|
+
def _resolve_templates(self, parameters: Dict[str, Any], connected_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
134
|
+
"""Resolve {{variable}} templates in parameters recursively."""
|
|
135
|
+
# Case-insensitive lookup
|
|
136
|
+
data_lower = {k.lower(): v for k, v in connected_data.items()}
|
|
137
|
+
|
|
138
|
+
# Log template resolution context at debug level
|
|
139
|
+
if logger.isEnabledFor(10): # DEBUG level
|
|
140
|
+
template_params = {k: v for k, v in parameters.items() if isinstance(v, str) and '{{' in v}
|
|
141
|
+
if template_params:
|
|
142
|
+
logger.debug(f"[ParameterResolver] Resolving templates: {list(template_params.keys())}")
|
|
143
|
+
|
|
144
|
+
def resolve(value: Any) -> Any:
|
|
145
|
+
if isinstance(value, str) and '{{' in value:
|
|
146
|
+
return self._resolve_string(value, data_lower)
|
|
147
|
+
if isinstance(value, dict):
|
|
148
|
+
return {k: resolve(v) for k, v in value.items()}
|
|
149
|
+
if isinstance(value, list):
|
|
150
|
+
return [resolve(item) for item in value]
|
|
151
|
+
return value
|
|
152
|
+
|
|
153
|
+
return {k: resolve(v) for k, v in parameters.items()}
|
|
154
|
+
|
|
155
|
+
def _resolve_string(self, value: str, data: Dict[str, Any]) -> Any:
|
|
156
|
+
"""Resolve templates in a string value."""
|
|
157
|
+
result = value
|
|
158
|
+
|
|
159
|
+
for match in TEMPLATE_PATTERN.finditer(value):
|
|
160
|
+
full_match = match.group(0)
|
|
161
|
+
path = match.group(1).split('.')
|
|
162
|
+
node_name = path[0].lower()
|
|
163
|
+
property_path = path[1:]
|
|
164
|
+
|
|
165
|
+
node_data = data.get(node_name)
|
|
166
|
+
resolved_value = self._navigate_path(node_data, property_path)
|
|
167
|
+
|
|
168
|
+
logger.debug(f"[ParameterResolver] Resolving '{full_match}': node_name={node_name}, path={property_path}, found_data={node_data is not None}, resolved={resolved_value is not None}")
|
|
169
|
+
|
|
170
|
+
if resolved_value is not None:
|
|
171
|
+
# If entire value is just the template, preserve type
|
|
172
|
+
if value.strip() == full_match:
|
|
173
|
+
return resolved_value
|
|
174
|
+
result = result.replace(full_match, str(resolved_value))
|
|
175
|
+
else:
|
|
176
|
+
# Log missing resolution for debugging
|
|
177
|
+
logger.debug(f"[ParameterResolver] Could not resolve '{full_match}': available keys={list(data.keys())}")
|
|
178
|
+
result = result.replace(full_match, '')
|
|
179
|
+
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
def _navigate_path(self, data: Any, path: List[str]) -> Any:
|
|
183
|
+
"""Navigate through nested dict/list using path parts.
|
|
184
|
+
|
|
185
|
+
Supports:
|
|
186
|
+
- Dict keys: 'field' -> data['field']
|
|
187
|
+
- Array indexing: 'items[0]' -> data['items'][0]
|
|
188
|
+
- Nested paths: 'messages[0].text' -> data['messages'][0]['text']
|
|
189
|
+
"""
|
|
190
|
+
current = data
|
|
191
|
+
for part in path:
|
|
192
|
+
if current is None:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
# Check for array index notation: field[index]
|
|
196
|
+
bracket_match = re.match(r'^(\w+)\[(\d+)\]$', part)
|
|
197
|
+
if bracket_match:
|
|
198
|
+
field_name = bracket_match.group(1)
|
|
199
|
+
index = int(bracket_match.group(2))
|
|
200
|
+
|
|
201
|
+
# Navigate to the field first
|
|
202
|
+
if isinstance(current, dict) and field_name in current:
|
|
203
|
+
current = current[field_name]
|
|
204
|
+
else:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
# Then access the array index
|
|
208
|
+
if isinstance(current, list) and 0 <= index < len(current):
|
|
209
|
+
current = current[index]
|
|
210
|
+
else:
|
|
211
|
+
return None
|
|
212
|
+
else:
|
|
213
|
+
# Standard dict key navigation
|
|
214
|
+
if not isinstance(current, dict) or part not in current:
|
|
215
|
+
return None
|
|
216
|
+
current = current[part]
|
|
217
|
+
|
|
218
|
+
return current
|