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,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
|
+
}
|