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.
Files changed (288) hide show
  1. package/.env.template +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/bin/cli.js +159 -0
  5. package/client/.dockerignore +45 -0
  6. package/client/Dockerfile +68 -0
  7. package/client/eslint.config.js +29 -0
  8. package/client/index.html +13 -0
  9. package/client/nginx.conf +66 -0
  10. package/client/package.json +48 -0
  11. package/client/src/App.tsx +27 -0
  12. package/client/src/Dashboard.tsx +1173 -0
  13. package/client/src/ParameterPanel.tsx +301 -0
  14. package/client/src/components/AIAgentNode.tsx +321 -0
  15. package/client/src/components/APIKeyValidator.tsx +118 -0
  16. package/client/src/components/ClaudeChatModelNode.tsx +18 -0
  17. package/client/src/components/ConditionalEdge.tsx +189 -0
  18. package/client/src/components/CredentialsModal.tsx +306 -0
  19. package/client/src/components/EdgeConditionEditor.tsx +443 -0
  20. package/client/src/components/GeminiChatModelNode.tsx +18 -0
  21. package/client/src/components/GenericNode.tsx +357 -0
  22. package/client/src/components/LocationParameterPanel.tsx +154 -0
  23. package/client/src/components/ModelNode.tsx +286 -0
  24. package/client/src/components/OpenAIChatModelNode.tsx +18 -0
  25. package/client/src/components/OutputPanel.tsx +471 -0
  26. package/client/src/components/ParameterRenderer.tsx +1874 -0
  27. package/client/src/components/SkillEditorModal.tsx +417 -0
  28. package/client/src/components/SquareNode.tsx +797 -0
  29. package/client/src/components/StartNode.tsx +250 -0
  30. package/client/src/components/ToolkitNode.tsx +365 -0
  31. package/client/src/components/TriggerNode.tsx +463 -0
  32. package/client/src/components/auth/LoginPage.tsx +247 -0
  33. package/client/src/components/auth/ProtectedRoute.tsx +59 -0
  34. package/client/src/components/base/BaseChatModelNode.tsx +271 -0
  35. package/client/src/components/icons/AIProviderIcons.tsx +50 -0
  36. package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
  37. package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
  38. package/client/src/components/maps/index.ts +26 -0
  39. package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
  40. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
  41. package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
  42. package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
  43. package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
  44. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
  45. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
  46. package/client/src/components/parameterPanel/index.ts +42 -0
  47. package/client/src/components/shared/DataPanel.tsx +142 -0
  48. package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
  49. package/client/src/components/ui/AIResultModal.tsx +204 -0
  50. package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
  51. package/client/src/components/ui/CodeEditor.tsx +81 -0
  52. package/client/src/components/ui/CollapsibleSection.tsx +88 -0
  53. package/client/src/components/ui/ComponentItem.tsx +154 -0
  54. package/client/src/components/ui/ComponentPalette.tsx +321 -0
  55. package/client/src/components/ui/ConsolePanel.tsx +1074 -0
  56. package/client/src/components/ui/ErrorBoundary.tsx +196 -0
  57. package/client/src/components/ui/InputNodesPanel.tsx +204 -0
  58. package/client/src/components/ui/MapSelector.tsx +314 -0
  59. package/client/src/components/ui/Modal.tsx +149 -0
  60. package/client/src/components/ui/NodeContextMenu.tsx +192 -0
  61. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
  62. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
  63. package/client/src/components/ui/SettingsPanel.tsx +243 -0
  64. package/client/src/components/ui/TopToolbar.tsx +736 -0
  65. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
  66. package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
  67. package/client/src/config/antdTheme.ts +186 -0
  68. package/client/src/config/api.ts +54 -0
  69. package/client/src/contexts/AuthContext.tsx +221 -0
  70. package/client/src/contexts/ThemeContext.tsx +42 -0
  71. package/client/src/contexts/WebSocketContext.tsx +1971 -0
  72. package/client/src/factories/baseChatModelFactory.ts +256 -0
  73. package/client/src/hooks/useAndroidOperations.ts +164 -0
  74. package/client/src/hooks/useApiKeyValidation.ts +107 -0
  75. package/client/src/hooks/useApiKeys.ts +238 -0
  76. package/client/src/hooks/useAppTheme.ts +17 -0
  77. package/client/src/hooks/useComponentPalette.ts +51 -0
  78. package/client/src/hooks/useCopyPaste.ts +155 -0
  79. package/client/src/hooks/useDragAndDrop.ts +124 -0
  80. package/client/src/hooks/useDragVariable.ts +88 -0
  81. package/client/src/hooks/useExecution.ts +313 -0
  82. package/client/src/hooks/useParameterPanel.ts +176 -0
  83. package/client/src/hooks/useReactFlowNodes.ts +189 -0
  84. package/client/src/hooks/useToolSchema.ts +209 -0
  85. package/client/src/hooks/useWhatsApp.ts +196 -0
  86. package/client/src/hooks/useWorkflowManagement.ts +46 -0
  87. package/client/src/index.css +315 -0
  88. package/client/src/main.tsx +19 -0
  89. package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
  90. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
  91. package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
  92. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
  93. package/client/src/nodeDefinitions/chatNodes.ts +135 -0
  94. package/client/src/nodeDefinitions/codeNodes.ts +54 -0
  95. package/client/src/nodeDefinitions/documentNodes.ts +379 -0
  96. package/client/src/nodeDefinitions/index.ts +15 -0
  97. package/client/src/nodeDefinitions/locationNodes.ts +463 -0
  98. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
  99. package/client/src/nodeDefinitions/skillNodes.ts +211 -0
  100. package/client/src/nodeDefinitions/toolNodes.ts +198 -0
  101. package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
  102. package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
  103. package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
  104. package/client/src/nodeDefinitions.ts +104 -0
  105. package/client/src/schemas/workflowSchema.ts +264 -0
  106. package/client/src/services/dynamicParameterService.ts +96 -0
  107. package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
  108. package/client/src/services/executionService.ts +232 -0
  109. package/client/src/services/workflowApi.ts +91 -0
  110. package/client/src/store/useAppStore.ts +582 -0
  111. package/client/src/styles/theme.ts +508 -0
  112. package/client/src/styles/zIndex.ts +17 -0
  113. package/client/src/types/ComponentTypes.ts +39 -0
  114. package/client/src/types/EdgeCondition.ts +231 -0
  115. package/client/src/types/INodeProperties.ts +288 -0
  116. package/client/src/types/NodeTypes.ts +28 -0
  117. package/client/src/utils/formatters.ts +33 -0
  118. package/client/src/utils/googleMapsLoader.ts +140 -0
  119. package/client/src/utils/locationUtils.ts +85 -0
  120. package/client/src/utils/nodeUtils.ts +31 -0
  121. package/client/src/utils/workflow.ts +30 -0
  122. package/client/src/utils/workflowExport.ts +120 -0
  123. package/client/src/vite-env.d.ts +12 -0
  124. package/client/tailwind.config.js +60 -0
  125. package/client/tsconfig.json +25 -0
  126. package/client/tsconfig.node.json +11 -0
  127. package/client/vite.config.js +35 -0
  128. package/docker-compose.prod.yml +107 -0
  129. package/docker-compose.yml +104 -0
  130. package/docs-MachinaOs/README.md +85 -0
  131. package/docs-MachinaOs/deployment/docker.mdx +228 -0
  132. package/docs-MachinaOs/deployment/production.mdx +345 -0
  133. package/docs-MachinaOs/docs.json +75 -0
  134. package/docs-MachinaOs/faq.mdx +309 -0
  135. package/docs-MachinaOs/favicon.svg +5 -0
  136. package/docs-MachinaOs/installation.mdx +160 -0
  137. package/docs-MachinaOs/introduction.mdx +114 -0
  138. package/docs-MachinaOs/logo/dark.svg +6 -0
  139. package/docs-MachinaOs/logo/light.svg +6 -0
  140. package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
  141. package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
  142. package/docs-MachinaOs/nodes/android.mdx +411 -0
  143. package/docs-MachinaOs/nodes/overview.mdx +181 -0
  144. package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
  145. package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
  146. package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
  147. package/docs-MachinaOs/quickstart.mdx +119 -0
  148. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
  149. package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
  150. package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
  151. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
  152. package/nul +0 -0
  153. package/package.json +70 -0
  154. package/scripts/build.js +158 -0
  155. package/scripts/check-ports.ps1 +33 -0
  156. package/scripts/clean.js +40 -0
  157. package/scripts/docker.js +93 -0
  158. package/scripts/kill-port.ps1 +154 -0
  159. package/scripts/start.js +210 -0
  160. package/scripts/stop.js +325 -0
  161. package/server/.dockerignore +44 -0
  162. package/server/Dockerfile +45 -0
  163. package/server/constants.py +249 -0
  164. package/server/core/__init__.py +1 -0
  165. package/server/core/cache.py +461 -0
  166. package/server/core/config.py +128 -0
  167. package/server/core/container.py +99 -0
  168. package/server/core/database.py +1211 -0
  169. package/server/core/logging.py +314 -0
  170. package/server/main.py +289 -0
  171. package/server/middleware/__init__.py +5 -0
  172. package/server/middleware/auth.py +89 -0
  173. package/server/models/__init__.py +1 -0
  174. package/server/models/auth.py +52 -0
  175. package/server/models/cache.py +24 -0
  176. package/server/models/database.py +211 -0
  177. package/server/models/nodes.py +455 -0
  178. package/server/package.json +9 -0
  179. package/server/pyproject.toml +72 -0
  180. package/server/requirements.txt +83 -0
  181. package/server/routers/__init__.py +1 -0
  182. package/server/routers/android.py +294 -0
  183. package/server/routers/auth.py +203 -0
  184. package/server/routers/database.py +151 -0
  185. package/server/routers/maps.py +142 -0
  186. package/server/routers/nodejs_compat.py +289 -0
  187. package/server/routers/webhook.py +90 -0
  188. package/server/routers/websocket.py +2127 -0
  189. package/server/routers/whatsapp.py +761 -0
  190. package/server/routers/workflow.py +200 -0
  191. package/server/services/__init__.py +1 -0
  192. package/server/services/ai.py +2415 -0
  193. package/server/services/android/__init__.py +27 -0
  194. package/server/services/android/broadcaster.py +114 -0
  195. package/server/services/android/client.py +608 -0
  196. package/server/services/android/manager.py +78 -0
  197. package/server/services/android/protocol.py +165 -0
  198. package/server/services/android_service.py +588 -0
  199. package/server/services/auth.py +131 -0
  200. package/server/services/chat_client.py +160 -0
  201. package/server/services/deployment/__init__.py +12 -0
  202. package/server/services/deployment/manager.py +706 -0
  203. package/server/services/deployment/state.py +47 -0
  204. package/server/services/deployment/triggers.py +275 -0
  205. package/server/services/event_waiter.py +785 -0
  206. package/server/services/execution/__init__.py +77 -0
  207. package/server/services/execution/cache.py +769 -0
  208. package/server/services/execution/conditions.py +373 -0
  209. package/server/services/execution/dlq.py +132 -0
  210. package/server/services/execution/executor.py +1351 -0
  211. package/server/services/execution/models.py +531 -0
  212. package/server/services/execution/recovery.py +235 -0
  213. package/server/services/handlers/__init__.py +126 -0
  214. package/server/services/handlers/ai.py +355 -0
  215. package/server/services/handlers/android.py +260 -0
  216. package/server/services/handlers/code.py +278 -0
  217. package/server/services/handlers/document.py +598 -0
  218. package/server/services/handlers/http.py +193 -0
  219. package/server/services/handlers/polyglot.py +105 -0
  220. package/server/services/handlers/tools.py +845 -0
  221. package/server/services/handlers/triggers.py +107 -0
  222. package/server/services/handlers/utility.py +822 -0
  223. package/server/services/handlers/whatsapp.py +476 -0
  224. package/server/services/maps.py +289 -0
  225. package/server/services/memory_store.py +103 -0
  226. package/server/services/node_executor.py +375 -0
  227. package/server/services/parameter_resolver.py +218 -0
  228. package/server/services/polyglot_client.py +169 -0
  229. package/server/services/scheduler.py +155 -0
  230. package/server/services/skill_loader.py +417 -0
  231. package/server/services/status_broadcaster.py +826 -0
  232. package/server/services/temporal/__init__.py +23 -0
  233. package/server/services/temporal/activities.py +344 -0
  234. package/server/services/temporal/client.py +76 -0
  235. package/server/services/temporal/executor.py +147 -0
  236. package/server/services/temporal/worker.py +251 -0
  237. package/server/services/temporal/workflow.py +355 -0
  238. package/server/services/temporal/ws_client.py +236 -0
  239. package/server/services/text.py +111 -0
  240. package/server/services/user_auth.py +172 -0
  241. package/server/services/websocket_client.py +29 -0
  242. package/server/services/workflow.py +597 -0
  243. package/server/skills/android-skill/SKILL.md +82 -0
  244. package/server/skills/assistant-personality/SKILL.md +45 -0
  245. package/server/skills/code-skill/SKILL.md +140 -0
  246. package/server/skills/http-skill/SKILL.md +161 -0
  247. package/server/skills/maps-skill/SKILL.md +170 -0
  248. package/server/skills/memory-skill/SKILL.md +154 -0
  249. package/server/skills/scheduler-skill/SKILL.md +84 -0
  250. package/server/skills/whatsapp-skill/SKILL.md +283 -0
  251. package/server/uv.lock +2916 -0
  252. package/server/whatsapp-rpc/.dockerignore +30 -0
  253. package/server/whatsapp-rpc/Dockerfile +44 -0
  254. package/server/whatsapp-rpc/Dockerfile.web +17 -0
  255. package/server/whatsapp-rpc/README.md +139 -0
  256. package/server/whatsapp-rpc/cli.js +95 -0
  257. package/server/whatsapp-rpc/configs/config.yaml +7 -0
  258. package/server/whatsapp-rpc/docker-compose.yml +35 -0
  259. package/server/whatsapp-rpc/docs/API.md +410 -0
  260. package/server/whatsapp-rpc/go.mod +67 -0
  261. package/server/whatsapp-rpc/go.sum +203 -0
  262. package/server/whatsapp-rpc/package.json +30 -0
  263. package/server/whatsapp-rpc/schema.json +1294 -0
  264. package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
  265. package/server/whatsapp-rpc/scripts/cli.js +162 -0
  266. package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
  267. package/server/whatsapp-rpc/src/go/config/config.go +49 -0
  268. package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
  269. package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
  270. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
  271. package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
  272. package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
  273. package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
  274. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
  275. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
  276. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
  277. package/server/whatsapp-rpc/web/app.py +609 -0
  278. package/server/whatsapp-rpc/web/requirements.txt +6 -0
  279. package/server/whatsapp-rpc/web/rpc_client.py +427 -0
  280. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
  281. package/server/whatsapp-rpc/web/templates/base.html +150 -0
  282. package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
  283. package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
  284. package/server/whatsapp-rpc/web/templates/groups.html +328 -0
  285. package/server/whatsapp-rpc/web/templates/messages.html +465 -0
  286. package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
  287. package/server/whatsapp-rpc/web/templates/send.html +259 -0
  288. package/server/whatsapp-rpc/web/templates/settings.html +459 -0
@@ -0,0 +1,845 @@
1
+ """Tool execution handlers for AI Agent tool calling.
2
+
3
+ This module contains handlers for executing tools called by the AI Agent.
4
+ Each tool type has its own handler function that processes the tool call
5
+ and returns results.
6
+ """
7
+
8
+ import math
9
+ import json
10
+ from typing import Dict, Any, Optional
11
+ from core.logging import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ async def execute_tool(tool_name: str, tool_args: Dict[str, Any],
17
+ config: Dict[str, Any]) -> Dict[str, Any]:
18
+ """Execute a tool by name using the appropriate handler.
19
+
20
+ This is the main dispatch function that routes tool calls to specific handlers
21
+ based on the node_type in the config.
22
+
23
+ Args:
24
+ tool_name: Name of the tool (for logging)
25
+ tool_args: Arguments provided by the AI model
26
+ config: Tool configuration containing node_type, node_id, parameters
27
+
28
+ Returns:
29
+ Tool execution result dict
30
+ """
31
+ node_type = config.get('node_type', '')
32
+
33
+ logger.info(f"[Tool] Executing tool '{tool_name}' (node_type: {node_type})")
34
+
35
+ # Calculator tool
36
+ if node_type == 'calculatorTool':
37
+ return await _execute_calculator(tool_args)
38
+
39
+ # HTTP Request tool (existing httpRequest node as tool)
40
+ if node_type in ('httpRequest', 'httpRequestTool'):
41
+ return await _execute_http_request(tool_args, config.get('parameters', {}))
42
+
43
+ # Python executor tool
44
+ if node_type == 'pythonExecutor':
45
+ return await _execute_python_code(tool_args, config.get('parameters', {}))
46
+
47
+ # Current time tool
48
+ if node_type == 'currentTimeTool':
49
+ return await _execute_current_time(tool_args, config.get('parameters', {}))
50
+
51
+ # Web search tool
52
+ if node_type == 'webSearchTool':
53
+ return await _execute_web_search(tool_args, config.get('parameters', {}))
54
+
55
+ # WhatsApp send (existing node used as tool)
56
+ if node_type == 'whatsappSend':
57
+ return await _execute_whatsapp_send(tool_args, config.get('parameters', {}))
58
+
59
+ # WhatsApp DB (existing node used as tool) - query contacts, groups, messages
60
+ if node_type == 'whatsappDb':
61
+ return await _execute_whatsapp_db(tool_args, config.get('parameters', {}))
62
+
63
+ # Android toolkit - routes to connected service nodes
64
+ if node_type == 'androidTool':
65
+ return await _execute_android_toolkit(tool_args, config)
66
+
67
+ # Google Maps Geocoding (addLocations node as tool)
68
+ if node_type == 'addLocations':
69
+ return await _execute_geocoding(tool_args, config.get('parameters', {}))
70
+
71
+ # Google Maps Nearby Places (showNearbyPlaces node as tool)
72
+ if node_type == 'showNearbyPlaces':
73
+ return await _execute_nearby_places(tool_args, config.get('parameters', {}))
74
+
75
+ # Generic fallback for unknown node types
76
+ logger.warning(f"[Tool] Unknown tool type: {node_type}, using generic handler")
77
+ return await _execute_generic(tool_args, config)
78
+
79
+
80
+ async def _execute_calculator(args: Dict[str, Any]) -> Dict[str, Any]:
81
+ """Execute calculator operations.
82
+
83
+ Supported operations: add, subtract, multiply, divide, power, sqrt, mod, abs
84
+
85
+ Args:
86
+ args: Dict with 'operation', 'a', and optionally 'b'
87
+
88
+ Returns:
89
+ Dict with operation, inputs, and result
90
+ """
91
+ operation = args.get('operation', '').lower()
92
+ a = float(args.get('a', 0))
93
+ b = float(args.get('b', 0))
94
+
95
+ operations = {
96
+ 'add': lambda: a + b,
97
+ 'subtract': lambda: a - b,
98
+ 'multiply': lambda: a * b,
99
+ 'divide': lambda: a / b if b != 0 else float('inf'),
100
+ 'power': lambda: math.pow(a, b),
101
+ 'sqrt': lambda: math.sqrt(abs(a)), # Use abs to handle negative
102
+ 'mod': lambda: a % b if b != 0 else 0,
103
+ 'abs': lambda: abs(a),
104
+ }
105
+
106
+ if operation not in operations:
107
+ return {
108
+ "error": f"Unknown operation: {operation}",
109
+ "supported_operations": list(operations.keys())
110
+ }
111
+
112
+ try:
113
+ result = operations[operation]()
114
+ logger.info(f"[Calculator] {operation}({a}, {b}) = {result}")
115
+ return {
116
+ "operation": operation,
117
+ "a": a,
118
+ "b": b,
119
+ "result": result
120
+ }
121
+ except Exception as e:
122
+ logger.error(f"[Calculator] Error: {e}")
123
+ return {"error": str(e)}
124
+
125
+
126
+ async def _execute_http_request(args: Dict[str, Any],
127
+ node_params: Dict[str, Any]) -> Dict[str, Any]:
128
+ """Execute HTTP request tool.
129
+
130
+ Args:
131
+ args: Dict with 'url', 'method', optionally 'body'
132
+ node_params: Node parameters containing base_url, headers, etc.
133
+
134
+ Returns:
135
+ Dict with status code, data, and url
136
+ """
137
+ import httpx
138
+
139
+ base_url = node_params.get('url', '')
140
+ url = args.get('url', '')
141
+ method = args.get('method', 'GET').upper()
142
+ body = args.get('body')
143
+
144
+ # Build full URL
145
+ if base_url and url and not url.startswith('http'):
146
+ full_url = f"{base_url.rstrip('/')}/{url.lstrip('/')}"
147
+ else:
148
+ full_url = url or base_url
149
+
150
+ if not full_url:
151
+ return {"error": "No URL provided"}
152
+
153
+ # Parse headers from node params
154
+ try:
155
+ default_headers = json.loads(node_params.get('headers', '{}'))
156
+ except:
157
+ default_headers = {}
158
+
159
+ logger.info(f"[HTTP Tool] {method} {full_url}")
160
+
161
+ try:
162
+ async with httpx.AsyncClient(timeout=30.0) as client:
163
+ response = await client.request(
164
+ method=method,
165
+ url=full_url,
166
+ headers=default_headers,
167
+ json=body if body else None
168
+ )
169
+
170
+ # Try to parse JSON response
171
+ try:
172
+ data = response.json()
173
+ except:
174
+ data = response.text
175
+
176
+ return {
177
+ "status": response.status_code,
178
+ "data": data,
179
+ "url": full_url,
180
+ "method": method
181
+ }
182
+
183
+ except httpx.TimeoutException:
184
+ return {"error": "Request timed out"}
185
+ except httpx.ConnectError as e:
186
+ return {"error": f"Connection failed: {str(e)}"}
187
+ except Exception as e:
188
+ logger.error(f"[HTTP Tool] Error: {e}")
189
+ return {"error": str(e)}
190
+
191
+
192
+ async def _execute_python_code(args: Dict[str, Any],
193
+ node_params: Dict[str, Any]) -> Dict[str, Any]:
194
+ """Execute Python code tool.
195
+
196
+ Args:
197
+ args: Dict with 'code'
198
+ node_params: Node parameters containing timeout, etc.
199
+
200
+ Returns:
201
+ Dict with result or output
202
+ """
203
+ import subprocess
204
+ import tempfile
205
+ import os
206
+
207
+ code = args.get('code', '')
208
+ timeout = int(node_params.get('timeout', 30))
209
+
210
+ if not code:
211
+ return {"error": "No code provided"}
212
+
213
+ # Create a temporary file with the code
214
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
215
+ # Wrap code to capture the result
216
+ wrapped_code = f"""
217
+ import json
218
+ import sys
219
+
220
+ def main():
221
+ {chr(10).join(' ' + line for line in code.split(chr(10)))}
222
+
223
+ try:
224
+ result = main()
225
+ if result is not None:
226
+ print(json.dumps({{"result": result}}, default=str))
227
+ else:
228
+ print(json.dumps({{"result": "Code executed successfully"}}))
229
+ except Exception as e:
230
+ print(json.dumps({{"error": str(e)}}))
231
+ """
232
+ f.write(wrapped_code)
233
+ temp_path = f.name
234
+
235
+ try:
236
+ logger.info(f"[Python Tool] Executing code (timeout: {timeout}s)")
237
+ result = subprocess.run(
238
+ ['python', temp_path],
239
+ capture_output=True,
240
+ text=True,
241
+ timeout=timeout
242
+ )
243
+
244
+ if result.returncode == 0:
245
+ try:
246
+ output = json.loads(result.stdout.strip())
247
+ return output
248
+ except:
249
+ return {"output": result.stdout.strip()}
250
+ else:
251
+ return {"error": result.stderr or "Code execution failed"}
252
+
253
+ except subprocess.TimeoutExpired:
254
+ return {"error": f"Code execution timed out after {timeout} seconds"}
255
+ except Exception as e:
256
+ logger.error(f"[Python Tool] Error: {e}")
257
+ return {"error": str(e)}
258
+ finally:
259
+ # Clean up temp file
260
+ try:
261
+ os.unlink(temp_path)
262
+ except:
263
+ pass
264
+
265
+
266
+ async def _execute_current_time(args: Dict[str, Any],
267
+ node_params: Dict[str, Any]) -> Dict[str, Any]:
268
+ """Get current date and time.
269
+
270
+ Args:
271
+ args: Dict with optional 'timezone'
272
+ node_params: Node parameters containing default timezone
273
+
274
+ Returns:
275
+ Dict with datetime, date, time, timezone, day_of_week, timestamp
276
+ """
277
+ from datetime import datetime
278
+ import pytz
279
+
280
+ timezone_str = args.get('timezone') or node_params.get('timezone', 'UTC')
281
+
282
+ try:
283
+ tz = pytz.timezone(timezone_str)
284
+ now = datetime.now(tz)
285
+
286
+ result = {
287
+ "datetime": now.isoformat(),
288
+ "date": now.strftime("%Y-%m-%d"),
289
+ "time": now.strftime("%H:%M:%S"),
290
+ "timezone": timezone_str,
291
+ "day_of_week": now.strftime("%A"),
292
+ "timestamp": int(now.timestamp())
293
+ }
294
+ logger.info(f"[CurrentTime] {timezone_str}: {result['datetime']}")
295
+ return result
296
+ except Exception as e:
297
+ logger.error(f"[CurrentTime] Error: {e}")
298
+ return {"error": f"Invalid timezone: {timezone_str}. Error: {str(e)}"}
299
+
300
+
301
+ async def _execute_web_search(args: Dict[str, Any],
302
+ node_params: Dict[str, Any]) -> Dict[str, Any]:
303
+ """Execute web search.
304
+
305
+ Args:
306
+ args: Dict with 'query'
307
+ node_params: Node parameters containing provider, apiKey, maxResults
308
+
309
+ Returns:
310
+ Dict with query, results list, provider
311
+ """
312
+ import httpx
313
+ import asyncio
314
+
315
+ query = args.get('query', '')
316
+ if not query:
317
+ return {"error": "No search query provided"}
318
+
319
+ provider = node_params.get('provider', 'duckduckgo')
320
+ max_results = int(node_params.get('maxResults', 5))
321
+
322
+ logger.info(f"[WebSearch] Searching '{query}' via {provider}")
323
+
324
+ try:
325
+ if provider == 'duckduckgo':
326
+ # Use the duckduckgo-search library for proper web search results
327
+ try:
328
+ from duckduckgo_search import DDGS
329
+
330
+ # Run synchronous DDGS in a thread pool to not block async
331
+ def do_search():
332
+ with DDGS() as ddgs:
333
+ return list(ddgs.text(query, max_results=max_results))
334
+
335
+ search_results = await asyncio.get_event_loop().run_in_executor(
336
+ None, do_search
337
+ )
338
+
339
+ results = []
340
+ for item in search_results:
341
+ results.append({
342
+ "title": item.get('title', ''),
343
+ "snippet": item.get('body', ''),
344
+ "url": item.get('href', '')
345
+ })
346
+
347
+ logger.info(f"[WebSearch] Found {len(results)} results via DuckDuckGo")
348
+ return {
349
+ "query": query,
350
+ "results": results,
351
+ "provider": "duckduckgo"
352
+ }
353
+
354
+ except ImportError:
355
+ logger.warning("[WebSearch] duckduckgo-search not installed, falling back to Instant Answer API")
356
+ # Fallback to Instant Answer API (limited results)
357
+ async with httpx.AsyncClient(timeout=10.0) as client:
358
+ response = await client.get(
359
+ "https://api.duckduckgo.com/",
360
+ params={"q": query, "format": "json", "no_html": 1}
361
+ )
362
+ data = response.json()
363
+
364
+ results = []
365
+ if data.get('AbstractText'):
366
+ results.append({
367
+ "title": data.get('Heading', 'Result'),
368
+ "snippet": data.get('AbstractText'),
369
+ "url": data.get('AbstractURL', '')
370
+ })
371
+
372
+ for topic in data.get('RelatedTopics', [])[:max_results]:
373
+ if isinstance(topic, dict) and 'Text' in topic:
374
+ results.append({
375
+ "title": topic.get('Text', '')[:50],
376
+ "snippet": topic.get('Text', ''),
377
+ "url": topic.get('FirstURL', '')
378
+ })
379
+
380
+ logger.info(f"[WebSearch] Found {len(results)} results (Instant Answer API fallback)")
381
+ return {
382
+ "query": query,
383
+ "results": results[:max_results],
384
+ "provider": "duckduckgo"
385
+ }
386
+
387
+ elif provider == 'serper':
388
+ api_key = node_params.get('apiKey', '')
389
+ if not api_key:
390
+ return {"error": "Serper API key required"}
391
+
392
+ async with httpx.AsyncClient(timeout=10.0) as client:
393
+ response = await client.post(
394
+ "https://google.serper.dev/search",
395
+ headers={"X-API-KEY": api_key, "Content-Type": "application/json"},
396
+ json={"q": query, "num": max_results}
397
+ )
398
+ data = response.json()
399
+
400
+ results = []
401
+ for item in data.get('organic', [])[:max_results]:
402
+ results.append({
403
+ "title": item.get('title', ''),
404
+ "snippet": item.get('snippet', ''),
405
+ "url": item.get('link', '')
406
+ })
407
+
408
+ logger.info(f"[WebSearch] Found {len(results)} results via Serper")
409
+ return {
410
+ "query": query,
411
+ "results": results,
412
+ "provider": "serper"
413
+ }
414
+
415
+ return {"error": f"Unknown search provider: {provider}"}
416
+
417
+ except Exception as e:
418
+ logger.error(f"[WebSearch] Error: {e}")
419
+ return {"error": f"Search failed: {str(e)}"}
420
+
421
+
422
+ async def _execute_whatsapp_send(args: Dict[str, Any],
423
+ node_params: Dict[str, Any]) -> Dict[str, Any]:
424
+ """Send WhatsApp message with full message type support.
425
+
426
+ Supports all message types: text, image, video, audio, document, sticker, location, contact
427
+ Recipients: phone number or group_id
428
+ Media sources: URL
429
+
430
+ Args:
431
+ args: LLM-provided arguments matching WhatsAppSendSchema (snake_case)
432
+ node_params: Node parameters (used as fallback)
433
+
434
+ Returns:
435
+ Dict with success status and message details
436
+ """
437
+ from services.handlers.whatsapp import handle_whatsapp_send
438
+
439
+ # Args are snake_case matching Pydantic schema and frontend node params
440
+ parameters = {
441
+ 'recipient_type': args.get('recipient_type', 'phone'),
442
+ 'phone': args.get('phone', ''),
443
+ 'group_id': args.get('group_id', ''),
444
+ 'message_type': args.get('message_type', 'text'),
445
+ 'message': args.get('message', ''),
446
+ 'media_source': 'url' if args.get('media_url') else 'none',
447
+ 'media_url': args.get('media_url', ''),
448
+ 'caption': args.get('caption', ''),
449
+ 'latitude': args.get('latitude'),
450
+ 'longitude': args.get('longitude'),
451
+ 'location_name': args.get('location_name', ''),
452
+ 'address': args.get('address', ''),
453
+ 'contact_name': args.get('contact_name', ''),
454
+ 'vcard': args.get('vcard', ''),
455
+ }
456
+
457
+ # Validate required fields based on message type
458
+ recipient_type = parameters['recipient_type']
459
+ message_type = parameters['message_type']
460
+
461
+ if recipient_type == 'phone' and not parameters['phone']:
462
+ return {"error": "Phone number is required for recipient_type='phone'"}
463
+ if recipient_type == 'group' and not parameters['group_id']:
464
+ return {"error": "Group ID is required for recipient_type='group'"}
465
+ if message_type == 'text' and not parameters['message']:
466
+ return {"error": "Message content is required for message_type='text'"}
467
+ if message_type in ('image', 'video', 'audio', 'document', 'sticker') and not parameters['media_url']:
468
+ return {"error": f"media_url is required for message_type='{message_type}'"}
469
+ if message_type == 'location' and (parameters['latitude'] is None or parameters['longitude'] is None):
470
+ return {"error": "latitude and longitude are required for message_type='location'"}
471
+ if message_type == 'contact' and not parameters['vcard']:
472
+ return {"error": "vcard is required for message_type='contact'"}
473
+
474
+ recipient = parameters['phone'] if recipient_type == 'phone' else parameters['group_id']
475
+ logger.info(f"[WhatsApp Tool] Sending {message_type} to {recipient[:15]}...")
476
+
477
+ try:
478
+ result = await handle_whatsapp_send(
479
+ node_id="tool_whatsapp_send",
480
+ node_type="whatsappSend",
481
+ parameters=parameters,
482
+ context={}
483
+ )
484
+
485
+ if result.get('success'):
486
+ return {
487
+ "success": True,
488
+ "recipient": recipient,
489
+ "recipient_type": recipient_type,
490
+ "message_type": message_type,
491
+ "details": result.get('result', {})
492
+ }
493
+ else:
494
+ return {"error": result.get('error', 'Unknown error')}
495
+
496
+ except Exception as e:
497
+ logger.error(f"[WhatsApp Tool] Error: {e}")
498
+ return {"error": f"WhatsApp send failed: {str(e)}"}
499
+
500
+
501
+ async def _execute_whatsapp_db(args: Dict[str, Any],
502
+ node_params: Dict[str, Any]) -> Dict[str, Any]:
503
+ """Query WhatsApp database - contacts, groups, messages.
504
+
505
+ Supports 6 operations:
506
+ - chat_history: Retrieve messages from a chat
507
+ - search_groups: Search groups by name
508
+ - get_group_info: Get group details with participant names
509
+ - get_contact_info: Get full contact info (for send/reply)
510
+ - list_contacts: List contacts with saved names
511
+ - check_contacts: Check WhatsApp registration status
512
+
513
+ Args:
514
+ args: LLM-provided arguments matching WhatsAppDbSchema (snake_case)
515
+ node_params: Node parameters (used as fallback)
516
+
517
+ Returns:
518
+ Dict with operation-specific results
519
+ """
520
+ from services.handlers.whatsapp import handle_whatsapp_db
521
+
522
+ operation = args.get('operation', 'chat_history')
523
+ logger.info(f"[WhatsApp DB Tool] Executing operation: {operation}")
524
+
525
+ # Build parameters for handler (snake_case matching frontend nodes)
526
+ parameters = {'operation': operation}
527
+
528
+ if operation == 'chat_history':
529
+ parameters.update({
530
+ 'chat_type': args.get('chat_type', 'individual'),
531
+ 'phone': args.get('phone', ''),
532
+ 'group_id': args.get('group_id', ''),
533
+ 'message_filter': args.get('message_filter', 'all'),
534
+ 'group_filter': args.get('group_filter', 'all'),
535
+ 'sender_phone': args.get('sender_phone', ''),
536
+ 'limit': args.get('limit', 50),
537
+ 'offset': args.get('offset', 0),
538
+ })
539
+ # Validate required fields
540
+ chat_type = parameters['chat_type']
541
+ if chat_type == 'individual' and not parameters['phone']:
542
+ return {"error": "Phone number is required for chat_type='individual'"}
543
+ if chat_type == 'group' and not parameters['group_id']:
544
+ return {"error": "Group ID is required for chat_type='group'"}
545
+
546
+ elif operation == 'search_groups':
547
+ parameters['query'] = args.get('query', '')
548
+ parameters['limit'] = min(args.get('limit', 20), 50) # Cap at 50 to prevent overflow
549
+
550
+ elif operation == 'get_group_info':
551
+ group_id = args.get('group_id', '')
552
+ if not group_id:
553
+ return {"error": "group_id is required for get_group_info"}
554
+ parameters['group_id_for_info'] = group_id
555
+ parameters['participant_limit'] = min(args.get('participant_limit', 50), 100) # Cap at 100
556
+
557
+ elif operation == 'get_contact_info':
558
+ phone = args.get('phone', '')
559
+ if not phone:
560
+ return {"error": "phone is required for get_contact_info"}
561
+ parameters['contact_phone'] = phone
562
+
563
+ elif operation == 'list_contacts':
564
+ parameters['query'] = args.get('query', '')
565
+ parameters['limit'] = min(args.get('limit', 50), 100) # Cap at 100 to prevent overflow
566
+
567
+ elif operation == 'check_contacts':
568
+ phones = args.get('phones', '')
569
+ if not phones:
570
+ return {"error": "phones (comma-separated) is required for check_contacts"}
571
+ parameters['phones'] = phones
572
+
573
+ else:
574
+ return {"error": f"Unknown operation: {operation}"}
575
+
576
+ try:
577
+ result = await handle_whatsapp_db(
578
+ node_id="tool_whatsapp_db",
579
+ node_type="whatsappDb",
580
+ parameters=parameters,
581
+ context={}
582
+ )
583
+
584
+ if result.get('success'):
585
+ # Return the result section for LLM consumption
586
+ return {
587
+ "success": True,
588
+ "operation": operation,
589
+ **result.get('result', {})
590
+ }
591
+ else:
592
+ return {"error": result.get('error', 'Unknown error')}
593
+
594
+ except Exception as e:
595
+ logger.error(f"[WhatsApp DB Tool] Error: {e}")
596
+ return {"error": f"WhatsApp DB operation failed: {str(e)}"}
597
+
598
+
599
+ async def _execute_android_toolkit(args: Dict[str, Any],
600
+ config: Dict[str, Any]) -> Dict[str, Any]:
601
+ """Execute Android toolkit by routing to connected service.
602
+
603
+ Follows n8n Sub-Node execution pattern - the toolkit routes
604
+ to the appropriate connected Android service node.
605
+
606
+ Uses the existing AndroidService which handles both relay (remote)
607
+ and local HTTP connections automatically.
608
+
609
+ Args:
610
+ args: LLM-provided arguments {service_id, action, parameters}
611
+ config: Toolkit config with connected_services list
612
+
613
+ Returns:
614
+ Service execution result
615
+ """
616
+ from services.android_service import AndroidService
617
+ from services.status_broadcaster import get_status_broadcaster
618
+
619
+ service_id = args.get('service_id', '')
620
+ action = args.get('action', '')
621
+ parameters = args.get('parameters') or {}
622
+
623
+ connected_services = config.get('connected_services', [])
624
+
625
+ # Validate service_id provided
626
+ if not service_id:
627
+ available = [s.get('service_id') or s.get('node_type') for s in connected_services]
628
+ return {
629
+ "error": "No service_id provided",
630
+ "hint": f"Available services: {', '.join(available)}" if available else "No services connected"
631
+ }
632
+
633
+ # Find matching connected service
634
+ target_service = None
635
+ for svc in connected_services:
636
+ svc_id = svc.get('service_id') or svc.get('node_type')
637
+ if svc_id == service_id:
638
+ target_service = svc
639
+ break
640
+
641
+ if not target_service:
642
+ available = [s.get('service_id') or s.get('node_type') for s in connected_services]
643
+ return {
644
+ "error": f"Service '{service_id}' not connected to toolkit",
645
+ "available_services": available
646
+ }
647
+
648
+ # Get connection parameters from connected Android node
649
+ svc_params = target_service.get('parameters', {})
650
+ host = svc_params.get('android_host', 'localhost')
651
+ port = int(svc_params.get('android_port', 8888))
652
+
653
+ # Use provided action, or fall back to node's default action
654
+ if not action:
655
+ action = svc_params.get('action') or target_service.get('action', 'status')
656
+
657
+ # Get the connected service's node_id for status broadcast
658
+ service_node_id = target_service.get('node_id')
659
+ # Get workflow_id from config for proper status scoping
660
+ workflow_id = config.get('workflow_id')
661
+
662
+ logger.info(f"[Android Toolkit] Executing {service_id}.{action} via '{target_service.get('label')}' (node: {service_node_id}, workflow: {workflow_id})")
663
+
664
+ # Broadcast executing status for the connected Android service node
665
+ # This makes the SquareNode show the animation
666
+ broadcaster = get_status_broadcaster()
667
+ if service_node_id:
668
+ await broadcaster.update_node_status(
669
+ service_node_id,
670
+ "executing",
671
+ {"message": f"Executing {action} via AI Agent toolkit"},
672
+ workflow_id=workflow_id
673
+ )
674
+
675
+ try:
676
+ # Use AndroidService which handles relay vs local connection automatically
677
+ android_service = AndroidService()
678
+ result = await android_service.execute_service(
679
+ node_id=config.get('node_id', 'toolkit'),
680
+ service_id=service_id,
681
+ action=action,
682
+ parameters=parameters,
683
+ android_host=host,
684
+ android_port=port
685
+ )
686
+
687
+ # Broadcast success/error status for the connected service node
688
+ if service_node_id:
689
+ if result.get('success'):
690
+ await broadcaster.update_node_status(
691
+ service_node_id,
692
+ "success",
693
+ {"message": f"{action} completed", "result": result.get('result', {})},
694
+ workflow_id=workflow_id
695
+ )
696
+ else:
697
+ await broadcaster.update_node_status(
698
+ service_node_id,
699
+ "error",
700
+ {"message": result.get('error', 'Unknown error')},
701
+ workflow_id=workflow_id
702
+ )
703
+
704
+ # Extract and return the relevant data
705
+ if result.get('success'):
706
+ return {
707
+ "success": True,
708
+ "service": service_id,
709
+ "action": action,
710
+ "data": result.get('result', {}).get('data', result.get('result', {}))
711
+ }
712
+ else:
713
+ return {
714
+ "error": result.get('error', 'Unknown error'),
715
+ "service": service_id,
716
+ "action": action
717
+ }
718
+
719
+ except Exception as e:
720
+ logger.error(f"[Android Toolkit] Unexpected error: {e}")
721
+ # Broadcast error status for the connected service node
722
+ if service_node_id:
723
+ await broadcaster.update_node_status(
724
+ service_node_id,
725
+ "error",
726
+ {"message": str(e)},
727
+ workflow_id=workflow_id
728
+ )
729
+ return {"error": str(e)}
730
+
731
+
732
+ async def _execute_geocoding(args: Dict[str, Any],
733
+ node_params: Dict[str, Any]) -> Dict[str, Any]:
734
+ """Execute Google Maps geocoding (addLocations node as tool).
735
+
736
+ Args:
737
+ args: LLM-provided arguments (snake_case: service_type, address, lat, lng)
738
+ node_params: Node parameters (may contain api_key)
739
+
740
+ Returns:
741
+ Geocoding result with coordinates or address
742
+ """
743
+ from services.handlers.utility import handle_add_locations
744
+ from core.container import container
745
+
746
+ # Args use snake_case matching Pydantic schema and node params
747
+ parameters = {**args, 'api_key': node_params.get('api_key', '')}
748
+
749
+ service_type = parameters.get('service_type', 'geocode')
750
+
751
+ # Validate required fields
752
+ if service_type == 'geocode' and not parameters.get('address'):
753
+ return {"error": "address is required for geocoding"}
754
+ if service_type == 'reverse_geocode':
755
+ if parameters.get('lat') is None or parameters.get('lng') is None:
756
+ return {"error": "lat and lng are required for reverse geocoding"}
757
+
758
+ lat, lng = parameters.get('lat'), parameters.get('lng')
759
+ location_str = parameters.get('address') or f"({lat}, {lng})"
760
+ logger.info(f"[Geocoding Tool] {service_type}: {location_str}")
761
+
762
+ try:
763
+ maps_service = container.maps_service()
764
+ result = await handle_add_locations(
765
+ node_id="tool_geocoding",
766
+ node_type="addLocations",
767
+ parameters=parameters,
768
+ context={},
769
+ maps_service=maps_service
770
+ )
771
+
772
+ if result.get('success'):
773
+ return {"success": True, "service_type": service_type, **result.get('result', {})}
774
+ else:
775
+ return {"error": result.get('error', 'Geocoding failed')}
776
+
777
+ except Exception as e:
778
+ logger.error(f"[Geocoding Tool] Error: {e}")
779
+ return {"error": f"Geocoding failed: {str(e)}"}
780
+
781
+
782
+ async def _execute_nearby_places(args: Dict[str, Any],
783
+ node_params: Dict[str, Any]) -> Dict[str, Any]:
784
+ """Execute Google Maps nearby places search (showNearbyPlaces node as tool).
785
+
786
+ Args:
787
+ args: LLM-provided arguments (snake_case: lat, lng, radius, type, keyword)
788
+ node_params: Node parameters (may contain api_key)
789
+
790
+ Returns:
791
+ Nearby places search results
792
+ """
793
+ from services.handlers.utility import handle_nearby_places
794
+ from core.container import container
795
+
796
+ # Args use snake_case matching Pydantic schema and node params
797
+ parameters = {**args, 'api_key': node_params.get('api_key', '')}
798
+
799
+ # Validate required fields
800
+ if parameters.get('lat') is None or parameters.get('lng') is None:
801
+ return {"error": "lat and lng are required for nearby places search"}
802
+
803
+ place_type = parameters.get('type', 'restaurant')
804
+ logger.info(f"[Nearby Places Tool] Searching {place_type} near ({parameters['lat']}, {parameters['lng']})")
805
+
806
+ try:
807
+ maps_service = container.maps_service()
808
+ result = await handle_nearby_places(
809
+ node_id="tool_nearby_places",
810
+ node_type="showNearbyPlaces",
811
+ parameters=parameters,
812
+ context={},
813
+ maps_service=maps_service
814
+ )
815
+
816
+ if result.get('success'):
817
+ return {"success": True, "type": place_type, **result.get('result', {})}
818
+ else:
819
+ return {"error": result.get('error', 'Nearby places search failed')}
820
+
821
+ except Exception as e:
822
+ logger.error(f"[Nearby Places Tool] Error: {e}")
823
+ return {"error": f"Nearby places search failed: {str(e)}"}
824
+
825
+
826
+ async def _execute_generic(args: Dict[str, Any],
827
+ config: Dict[str, Any]) -> Dict[str, Any]:
828
+ """Execute a generic tool (fallback handler).
829
+
830
+ For node types without specific handlers, this returns the input
831
+ along with node information.
832
+
833
+ Args:
834
+ args: Tool arguments
835
+ config: Tool configuration
836
+
837
+ Returns:
838
+ Dict with input echoed and node info
839
+ """
840
+ return {
841
+ "input": args.get('input', ''),
842
+ "node_type": config.get('node_type'),
843
+ "node_id": config.get('node_id'),
844
+ "message": "Generic tool executed - no specific handler for this node type"
845
+ }