machinaos 0.0.10 → 0.0.13
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 +16 -0
- package/client/package.json +1 -1
- package/client/src/Dashboard.tsx +3 -3
- package/client/src/components/AIAgentNode.tsx +24 -12
- package/client/src/components/OutputPanel.tsx +3 -2
- package/client/src/components/parameterPanel/InputSection.tsx +16 -3
- package/client/src/nodeDefinitions/aiAgentNodes.ts +12 -0
- package/client/src/nodeDefinitions/specializedAgentNodes.ts +68 -320
- package/client/src/nodeDefinitions/toolNodes.ts +87 -1
- package/client/src/nodeDefinitions/workflowNodes.ts +55 -1
- package/package.json +12 -3
- package/scripts/daemon.js +427 -0
- package/scripts/start.js +7 -1
- package/scripts/sync-version.js +108 -0
- package/server/Dockerfile +6 -7
- package/server/constants.py +2 -0
- package/server/core/cleanup.py +123 -0
- package/server/core/config.py +16 -0
- package/server/core/database.py +92 -1
- package/server/core/health.py +121 -0
- package/server/examples/__init__.py +1 -0
- package/server/gunicorn.conf.py +46 -0
- package/server/main.py +38 -3
- package/server/models/database.py +1 -0
- package/server/models/nodes.py +18 -2
- package/server/requirements-docker.txt +86 -0
- package/server/routers/database.py +16 -0
- package/server/routers/websocket.py +6 -5
- package/server/services/ai.py +115 -14
- package/server/services/auth.py +6 -1
- package/server/services/deployment/manager.py +14 -0
- package/server/services/event_waiter.py +55 -0
- package/server/services/example_loader.py +60 -0
- package/server/services/execution/executor.py +2 -0
- package/server/services/execution/models.py +8 -0
- package/server/services/handlers/__init__.py +2 -0
- package/server/services/handlers/ai.py +164 -11
- package/server/services/handlers/document.py +13 -4
- package/server/services/handlers/tools.py +445 -14
- package/server/services/node_executor.py +3 -0
- package/server/services/temporal/activities.py +3 -0
- package/server/services/workflow.py +2 -0
- package/server/skills/android_agent/app-launcher-skill/SKILL.md +137 -0
- package/server/skills/android_agent/app-list-skill/SKILL.md +148 -0
- package/server/skills/android_agent/audio-skill/SKILL.md +169 -0
- package/server/skills/android_agent/battery-skill/SKILL.md +114 -0
- package/server/skills/android_agent/bluetooth-skill/SKILL.md +151 -0
- package/server/skills/android_agent/camera-skill/SKILL.md +148 -0
- package/server/skills/android_agent/environmental-skill/SKILL.md +140 -0
- package/server/skills/android_agent/location-skill/SKILL.md +163 -0
- package/server/skills/android_agent/motion-skill/SKILL.md +141 -0
- package/server/skills/android_agent/screen-control-skill/SKILL.md +164 -0
- package/server/skills/android_agent/wifi-skill/SKILL.md +182 -0
- package/server/skills/assistant/subagent-skill/SKILL.md +205 -0
- package/server/skills/coding_agent/javascript-skill/SKILL.md +196 -0
- package/server/skills/coding_agent/python-skill/SKILL.md +165 -0
- package/server/skills/social_agent/whatsapp-db-skill/SKILL.md +284 -0
- package/server/skills/social_agent/whatsapp-send-skill/SKILL.md +180 -0
- package/server/skills/task_agent/cron-scheduler-skill/SKILL.md +215 -0
- package/server/skills/task_agent/task-manager-skill/SKILL.md +251 -0
- package/server/skills/task_agent/timer-skill/SKILL.md +168 -0
- package/server/skills/travel_agent/geocoding-skill/SKILL.md +186 -0
- package/server/skills/travel_agent/nearby-places-skill/SKILL.md +234 -0
- package/server/skills/web_agent/http-request-skill/SKILL.md +211 -0
- package/server/skills/android/skill/SKILL.md +0 -84
- package/server/skills/assistant/code-skill/SKILL.md +0 -176
- package/server/skills/assistant/http-skill/SKILL.md +0 -163
- package/server/skills/assistant/maps-skill/SKILL.md +0 -172
- package/server/skills/assistant/scheduler-skill/SKILL.md +0 -86
- package/server/skills/assistant/whatsapp-skill/SKILL.md +0 -285
- /package/server/skills/{android → android_agent}/personality/SKILL.md +0 -0
- /package/server/skills/{assistant → web_agent}/web-search-skill/SKILL.md +0 -0
|
@@ -244,6 +244,7 @@ async def handle_execute_node(data: Dict[str, Any], websocket: WebSocket) -> Dic
|
|
|
244
244
|
nodes=data.get("nodes", []), edges=data.get("edges", []),
|
|
245
245
|
session_id=data.get("session_id", "default"),
|
|
246
246
|
workflow_id=workflow_id,
|
|
247
|
+
outputs=data.get("outputs", {}), # Upstream node outputs for data flow
|
|
247
248
|
)
|
|
248
249
|
|
|
249
250
|
if result.get("success"):
|
|
@@ -628,20 +629,20 @@ async def handle_deploy_workflow(data: Dict[str, Any], websocket: WebSocket) ->
|
|
|
628
629
|
session_id = data.get("session_id", "default")
|
|
629
630
|
|
|
630
631
|
# DEBUG: Log received edges to trace tool connection issues
|
|
631
|
-
logger.
|
|
632
|
+
logger.debug(f"[Deploy] Received {len(edges)} edges for workflow {workflow_id}")
|
|
632
633
|
for e in edges:
|
|
633
634
|
target_handle = e.get('targetHandle')
|
|
634
635
|
if target_handle and target_handle.startswith('input-') and target_handle != 'input-main':
|
|
635
|
-
logger.
|
|
636
|
+
logger.debug(f"[Deploy] Config edge: {e.get('source')} -> {e.get('target')} (handle={target_handle})")
|
|
636
637
|
|
|
637
638
|
# Check for tool connections to AI Agent
|
|
638
639
|
tool_edges = [e for e in edges if e.get('targetHandle') == 'input-tools']
|
|
639
640
|
if tool_edges:
|
|
640
|
-
logger.
|
|
641
|
+
logger.debug(f"[Deploy] Tool edges found: {len(tool_edges)}")
|
|
641
642
|
for te in tool_edges:
|
|
642
|
-
logger.
|
|
643
|
+
logger.debug(f"[Deploy] Tool edge: source={te.get('source')} -> target={te.get('target')}")
|
|
643
644
|
else:
|
|
644
|
-
logger.
|
|
645
|
+
logger.debug(f"[Deploy] No input-tools edges found")
|
|
645
646
|
|
|
646
647
|
if not nodes:
|
|
647
648
|
return {"success": False, "error": "No nodes provided"}
|
package/server/services/ai.py
CHANGED
|
@@ -1402,6 +1402,20 @@ class AIService:
|
|
|
1402
1402
|
|
|
1403
1403
|
logger.debug(f"[LangGraph] Built {len(tools)} tools")
|
|
1404
1404
|
|
|
1405
|
+
# Auto-inject check_delegated_tasks tool when delegation tools present
|
|
1406
|
+
if any(name.startswith('delegate_to_') for name in tool_configs):
|
|
1407
|
+
check_info = {
|
|
1408
|
+
'node_type': '_builtin_check_delegated_tasks',
|
|
1409
|
+
'node_id': f'{node_id}_check_tasks',
|
|
1410
|
+
'parameters': {},
|
|
1411
|
+
'label': 'Check Delegated Tasks',
|
|
1412
|
+
}
|
|
1413
|
+
check_tool, check_config = await self._build_tool_from_node(check_info)
|
|
1414
|
+
if check_tool:
|
|
1415
|
+
tools.append(check_tool)
|
|
1416
|
+
tool_configs[check_tool.name] = check_config
|
|
1417
|
+
logger.debug(f"[LangGraph] Auto-injected check_delegated_tasks tool")
|
|
1418
|
+
|
|
1405
1419
|
# Create tool executor callback
|
|
1406
1420
|
async def tool_executor(tool_name: str, tool_args: Dict) -> Any:
|
|
1407
1421
|
"""Execute a tool by name."""
|
|
@@ -1438,6 +1452,7 @@ class AIService:
|
|
|
1438
1452
|
# These allow child agents to execute with their own tools
|
|
1439
1453
|
config['ai_service'] = self
|
|
1440
1454
|
config['database'] = self.database
|
|
1455
|
+
config['parent_node_id'] = node_id # For delegation result tracking
|
|
1441
1456
|
if context:
|
|
1442
1457
|
config['nodes'] = context.get('nodes', [])
|
|
1443
1458
|
config['edges'] = context.get('edges', [])
|
|
@@ -1709,6 +1724,20 @@ class AIService:
|
|
|
1709
1724
|
|
|
1710
1725
|
logger.debug(f"[ChatAgent] Built {len(all_tools)} tools from tool_data")
|
|
1711
1726
|
|
|
1727
|
+
# Auto-inject check_delegated_tasks tool when delegation tools present
|
|
1728
|
+
if any(name.startswith('delegate_to_') for name in tool_node_configs):
|
|
1729
|
+
check_info = {
|
|
1730
|
+
'node_type': '_builtin_check_delegated_tasks',
|
|
1731
|
+
'node_id': f'{node_id}_check_tasks',
|
|
1732
|
+
'parameters': {},
|
|
1733
|
+
'label': 'Check Delegated Tasks',
|
|
1734
|
+
}
|
|
1735
|
+
check_tool, check_config = await self._build_tool_from_node(check_info)
|
|
1736
|
+
if check_tool:
|
|
1737
|
+
all_tools.append(check_tool)
|
|
1738
|
+
tool_node_configs[check_tool.name] = check_config
|
|
1739
|
+
logger.debug(f"[ChatAgent] Auto-injected check_delegated_tasks tool")
|
|
1740
|
+
|
|
1712
1741
|
logger.debug(f"[ChatAgent] Total tools available: {len(all_tools)}")
|
|
1713
1742
|
# Debug: log all tool schemas to verify they're correct
|
|
1714
1743
|
for t in all_tools:
|
|
@@ -1861,6 +1890,7 @@ class AIService:
|
|
|
1861
1890
|
# These allow child agents to execute with their own tools
|
|
1862
1891
|
config['ai_service'] = self
|
|
1863
1892
|
config['database'] = self.database
|
|
1893
|
+
config['parent_node_id'] = node_id # For delegation result tracking
|
|
1864
1894
|
if context:
|
|
1865
1895
|
config['nodes'] = context.get('nodes', [])
|
|
1866
1896
|
config['edges'] = context.get('edges', [])
|
|
@@ -2062,8 +2092,12 @@ class AIService:
|
|
|
2062
2092
|
'whatsappDb': 'whatsapp_db',
|
|
2063
2093
|
'gmaps_locations': 'geocode',
|
|
2064
2094
|
'gmaps_nearby_places': 'nearby_places',
|
|
2095
|
+
'taskManager': 'task_manager',
|
|
2065
2096
|
'timer': 'timer',
|
|
2066
2097
|
'cronScheduler': 'cron_scheduler',
|
|
2098
|
+
# Built-in check tool for delegation results
|
|
2099
|
+
'_builtin_check_delegated_tasks': 'check_delegated_tasks',
|
|
2100
|
+
# Agent delegation tools
|
|
2067
2101
|
'aiAgent': 'delegate_to_ai_agent',
|
|
2068
2102
|
'chatAgent': 'delegate_to_chat_agent',
|
|
2069
2103
|
'android_agent': 'delegate_to_android_agent',
|
|
@@ -2105,20 +2139,24 @@ class AIService:
|
|
|
2105
2139
|
'whatsappDb': 'Query WhatsApp database - list contacts, search groups, get contact/group info, retrieve chat history.',
|
|
2106
2140
|
'gmaps_locations': 'Geocode addresses to coordinates or reverse geocode coordinates to addresses using Google Maps.',
|
|
2107
2141
|
'gmaps_nearby_places': 'Search for nearby places (restaurants, hospitals, banks, etc.) using Google Maps Places API.',
|
|
2142
|
+
'taskManager': 'Track delegated sub-agent tasks. Operations: list_tasks (see all tasks), get_task (check specific task status/result), mark_done (cleanup completed tasks).',
|
|
2108
2143
|
'timer': 'Wait/sleep for a specified duration. Specify duration (1-3600) and unit (seconds, minutes, or hours). Returns timestamp and elapsed time after waiting.',
|
|
2109
2144
|
'cronScheduler': 'Schedule a delayed or recurring execution. Supports seconds, minutes, hours, daily, weekly, monthly frequencies with timezone. Use frequency to set schedule type, then set the relevant interval/time parameters.',
|
|
2110
|
-
|
|
2111
|
-
'
|
|
2112
|
-
|
|
2113
|
-
'
|
|
2114
|
-
'
|
|
2115
|
-
'
|
|
2116
|
-
'
|
|
2117
|
-
'
|
|
2118
|
-
'
|
|
2119
|
-
'
|
|
2120
|
-
'
|
|
2121
|
-
'
|
|
2145
|
+
# Built-in check tool for delegation results
|
|
2146
|
+
'_builtin_check_delegated_tasks': 'Check status and retrieve results of previously delegated tasks.',
|
|
2147
|
+
# Agent delegation tools - ONE-SHOT fire-and-forget pattern
|
|
2148
|
+
'aiAgent': 'ONE-SHOT delegation to AI Agent. Call ONCE per task, returns task_id immediately. Agent works in background - do NOT re-call.',
|
|
2149
|
+
'chatAgent': 'ONE-SHOT delegation to Chat Agent. Call ONCE per task, returns task_id immediately. Agent works in background - do NOT re-call.',
|
|
2150
|
+
'android_agent': 'ONE-SHOT delegation to Android Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
|
|
2151
|
+
'coding_agent': 'ONE-SHOT delegation to Coding Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
|
|
2152
|
+
'web_agent': 'ONE-SHOT delegation to Web Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
|
|
2153
|
+
'task_agent': 'ONE-SHOT delegation to Task Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
|
|
2154
|
+
'social_agent': 'ONE-SHOT delegation to Social Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
|
|
2155
|
+
'travel_agent': 'ONE-SHOT delegation to Travel Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
|
|
2156
|
+
'tool_agent': 'ONE-SHOT delegation to Tool Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
|
|
2157
|
+
'productivity_agent': 'ONE-SHOT delegation to Productivity Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
|
|
2158
|
+
'payments_agent': 'ONE-SHOT delegation to Payments Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
|
|
2159
|
+
'consumer_agent': 'ONE-SHOT delegation to Consumer Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
|
|
2122
2160
|
# Android service nodes (direct tool usage)
|
|
2123
2161
|
'batteryMonitor': 'Monitor Android battery status, level, charging state, temperature, and health.',
|
|
2124
2162
|
'networkMonitor': 'Monitor Android network connectivity, type, and internet availability.',
|
|
@@ -2174,6 +2212,29 @@ class AIService:
|
|
|
2174
2212
|
service_names = [s.get('label') or s.get('service_id', 'unknown') for s in connected_services]
|
|
2175
2213
|
tool_description = f"{tool_description} Connected: {', '.join(service_names)}"
|
|
2176
2214
|
|
|
2215
|
+
# For AI Agent nodes, enhance description with child agent's tool capabilities
|
|
2216
|
+
# This allows parent agent to know what the child agent can do
|
|
2217
|
+
from constants import AI_AGENT_TYPES
|
|
2218
|
+
if node_type in AI_AGENT_TYPES:
|
|
2219
|
+
child_tools = tool_info.get('child_tools', [])
|
|
2220
|
+
if child_tools:
|
|
2221
|
+
# Build capability description from child's connected tools
|
|
2222
|
+
capability_descriptions = []
|
|
2223
|
+
for child_tool in child_tools:
|
|
2224
|
+
child_type = child_tool.get('node_type', '')
|
|
2225
|
+
child_label = child_tool.get('label', child_type)
|
|
2226
|
+
# Get the tool description from DEFAULT_TOOL_DESCRIPTIONS
|
|
2227
|
+
child_desc = DEFAULT_TOOL_DESCRIPTIONS.get(child_type, f"Use {child_label}")
|
|
2228
|
+
capability_descriptions.append(f"- {child_label}: {child_desc}")
|
|
2229
|
+
|
|
2230
|
+
capabilities_text = "\n".join(capability_descriptions)
|
|
2231
|
+
tool_description = (
|
|
2232
|
+
f"Delegate tasks to '{node_label}' agent. "
|
|
2233
|
+
f"This agent has the following capabilities:\n{capabilities_text}\n"
|
|
2234
|
+
f"Call ONCE per task, returns task_id. Agent works in background."
|
|
2235
|
+
)
|
|
2236
|
+
logger.info(f"[LangGraph] Enhanced tool description for {node_type} with {len(child_tools)} child tools")
|
|
2237
|
+
|
|
2177
2238
|
# Clean tool name (LangChain requires alphanumeric + underscores)
|
|
2178
2239
|
import re
|
|
2179
2240
|
tool_name = re.sub(r'[^a-zA-Z0-9_]', '_', tool_name)
|
|
@@ -2615,6 +2676,45 @@ class AIService:
|
|
|
2615
2676
|
|
|
2616
2677
|
return NearbyPlacesSchema
|
|
2617
2678
|
|
|
2679
|
+
# Task Manager schema (dual-purpose: AI tool + workflow node)
|
|
2680
|
+
if node_type == 'taskManager':
|
|
2681
|
+
class TaskManagerSchema(BaseModel):
|
|
2682
|
+
"""Manage delegated tasks - list, check status, mark done.
|
|
2683
|
+
|
|
2684
|
+
Use this to track sub-agent tasks you've delegated.
|
|
2685
|
+
- list_tasks: See all active and completed tasks
|
|
2686
|
+
- get_task: Get details on a specific task
|
|
2687
|
+
- mark_done: Clean up a completed task from tracking
|
|
2688
|
+
"""
|
|
2689
|
+
operation: str = Field(
|
|
2690
|
+
description="Operation: 'list_tasks', 'get_task', or 'mark_done'"
|
|
2691
|
+
)
|
|
2692
|
+
task_id: Optional[str] = Field(
|
|
2693
|
+
default=None,
|
|
2694
|
+
description="Task ID (required for get_task/mark_done)"
|
|
2695
|
+
)
|
|
2696
|
+
status_filter: Optional[str] = Field(
|
|
2697
|
+
default=None,
|
|
2698
|
+
description="Filter by status: 'running', 'completed', or 'error' (for list_tasks)"
|
|
2699
|
+
)
|
|
2700
|
+
|
|
2701
|
+
return TaskManagerSchema
|
|
2702
|
+
|
|
2703
|
+
# Check delegated tasks schema (built-in tool for result retrieval)
|
|
2704
|
+
if node_type == '_builtin_check_delegated_tasks':
|
|
2705
|
+
class CheckDelegatedTasksSchema(BaseModel):
|
|
2706
|
+
"""Check on previously delegated tasks and retrieve their results.
|
|
2707
|
+
|
|
2708
|
+
Call this to see if delegated agents have completed their work.
|
|
2709
|
+
Returns status and results for each task.
|
|
2710
|
+
"""
|
|
2711
|
+
task_ids: Optional[List[str]] = Field(
|
|
2712
|
+
default=None,
|
|
2713
|
+
description="Specific task IDs to check. Omit to get ALL delegated tasks."
|
|
2714
|
+
)
|
|
2715
|
+
|
|
2716
|
+
return CheckDelegatedTasksSchema
|
|
2717
|
+
|
|
2618
2718
|
# AI Agent delegation schema (fire-and-forget async delegation)
|
|
2619
2719
|
if node_type in ('aiAgent', 'chatAgent', 'android_agent', 'coding_agent', 'web_agent', 'task_agent', 'social_agent', 'travel_agent', 'tool_agent', 'productivity_agent', 'payments_agent', 'consumer_agent'):
|
|
2620
2720
|
agent_label = params.get('label', node_type)
|
|
@@ -2622,8 +2722,9 @@ class AIService:
|
|
|
2622
2722
|
class DelegateToAgentSchema(BaseModel):
|
|
2623
2723
|
"""Delegate a task to another AI Agent (non-blocking).
|
|
2624
2724
|
|
|
2625
|
-
The child agent
|
|
2626
|
-
|
|
2725
|
+
The child agent works independently in the background.
|
|
2726
|
+
Returns a task_id immediately. Use 'check_delegated_tasks'
|
|
2727
|
+
tool to check status and retrieve results when ready.
|
|
2627
2728
|
"""
|
|
2628
2729
|
task: str = Field(
|
|
2629
2730
|
description=f"The task/instruction to delegate to '{agent_label}'. Be specific about what you want done."
|
package/server/services/auth.py
CHANGED
|
@@ -86,7 +86,12 @@ class AuthService:
|
|
|
86
86
|
api_key_record = await self.database.get_api_key_by_provider(provider, session_id)
|
|
87
87
|
if api_key_record and api_key_record.is_valid:
|
|
88
88
|
# Check if not expired (30 days)
|
|
89
|
-
|
|
89
|
+
# Ensure both datetimes are timezone-aware for comparison
|
|
90
|
+
# SQLite may return offset-naive datetimes
|
|
91
|
+
last_validated = api_key_record.last_validated
|
|
92
|
+
if last_validated.tzinfo is None:
|
|
93
|
+
last_validated = last_validated.replace(tzinfo=timezone.utc)
|
|
94
|
+
if datetime.now(timezone.utc) - last_validated < timedelta(days=30):
|
|
90
95
|
return self.decrypt_api_key(api_key_record.key_encrypted)
|
|
91
96
|
|
|
92
97
|
return None
|
|
@@ -629,6 +629,20 @@ class DeploymentManager:
|
|
|
629
629
|
downstream_ids.add(source)
|
|
630
630
|
logger.debug(f"[Deployment] Including sub-node {source} connected to toolkit {target}")
|
|
631
631
|
|
|
632
|
+
# Include tool nodes connected to AI Agent nodes (for capability discovery)
|
|
633
|
+
# When a child agent is included, we need its connected tools so the parent
|
|
634
|
+
# can discover what capabilities the child has
|
|
635
|
+
from constants import AI_AGENT_TYPES
|
|
636
|
+
agent_node_ids = {n['id'] for n in nodes if n.get('type') in AI_AGENT_TYPES and n['id'] in downstream_ids}
|
|
637
|
+
for edge in edges:
|
|
638
|
+
target = edge.get('target')
|
|
639
|
+
source = edge.get('source')
|
|
640
|
+
target_handle = edge.get('targetHandle', '')
|
|
641
|
+
# Include tool nodes connected to agent's input-tools handle
|
|
642
|
+
if target in agent_node_ids and target_handle == 'input-tools' and source not in downstream_ids:
|
|
643
|
+
downstream_ids.add(source)
|
|
644
|
+
logger.debug(f"[Deployment] Including tool node {source} connected to agent {target}")
|
|
645
|
+
|
|
632
646
|
return [n for n in nodes if n['id'] in downstream_ids]
|
|
633
647
|
|
|
634
648
|
# =========================================================================
|
|
@@ -105,6 +105,11 @@ TRIGGER_REGISTRY: Dict[str, TriggerConfig] = {
|
|
|
105
105
|
event_type='chat_message_received',
|
|
106
106
|
display_name='Chat Message'
|
|
107
107
|
),
|
|
108
|
+
'taskTrigger': TriggerConfig(
|
|
109
|
+
node_type='taskTrigger',
|
|
110
|
+
event_type='task_completed',
|
|
111
|
+
display_name='Task Completed'
|
|
112
|
+
),
|
|
108
113
|
# Future triggers - just add to registry:
|
|
109
114
|
# 'emailTrigger': TriggerConfig('emailTrigger', 'email_received', 'Email'),
|
|
110
115
|
# 'mqttTrigger': TriggerConfig('mqttTrigger', 'mqtt_message', 'MQTT Message'),
|
|
@@ -282,11 +287,61 @@ def build_chat_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
|
282
287
|
return matches
|
|
283
288
|
|
|
284
289
|
|
|
290
|
+
def build_task_completed_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
291
|
+
"""Build filter function for task completed events.
|
|
292
|
+
|
|
293
|
+
Filters by:
|
|
294
|
+
- task_id: Specific task ID to watch (optional)
|
|
295
|
+
- agent_name: Filter by child agent name (optional)
|
|
296
|
+
- status_filter: 'all', 'completed', 'error' (default: 'all')
|
|
297
|
+
- parent_node_id: Filter by parent node (optional, for scoping)
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
params: Node parameters
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Filter function that checks if event matches criteria
|
|
304
|
+
"""
|
|
305
|
+
task_id_filter = params.get('task_id', '')
|
|
306
|
+
agent_name_filter = params.get('agent_name', '')
|
|
307
|
+
status_filter = params.get('status_filter', 'all') # all, completed, error
|
|
308
|
+
parent_node_id = params.get('parent_node_id', '')
|
|
309
|
+
|
|
310
|
+
def matches(data: Dict) -> bool:
|
|
311
|
+
# Task ID filter (exact match if specified)
|
|
312
|
+
if task_id_filter:
|
|
313
|
+
if data.get('task_id') != task_id_filter:
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
# Agent name filter (contains match)
|
|
317
|
+
if agent_name_filter:
|
|
318
|
+
event_agent = data.get('agent_name', '')
|
|
319
|
+
if agent_name_filter.lower() not in event_agent.lower():
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
# Status filter
|
|
323
|
+
event_status = data.get('status', '')
|
|
324
|
+
if status_filter == 'completed' and event_status != 'completed':
|
|
325
|
+
return False
|
|
326
|
+
if status_filter == 'error' and event_status != 'error':
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
# Parent node filter (for scoping to specific parent agent)
|
|
330
|
+
if parent_node_id:
|
|
331
|
+
if data.get('parent_node_id') != parent_node_id:
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
return True
|
|
335
|
+
|
|
336
|
+
return matches
|
|
337
|
+
|
|
338
|
+
|
|
285
339
|
# Registry of filter builders per trigger type
|
|
286
340
|
FILTER_BUILDERS: Dict[str, Callable[[Dict], Callable[[Dict], bool]]] = {
|
|
287
341
|
'whatsappReceive': build_whatsapp_filter,
|
|
288
342
|
'webhookTrigger': build_webhook_filter,
|
|
289
343
|
'chatTrigger': build_chat_filter,
|
|
344
|
+
'taskTrigger': build_task_completed_filter,
|
|
290
345
|
}
|
|
291
346
|
|
|
292
347
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Example workflow loader - reuses existing database.save_workflow()"""
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
# Workflows folder at project root (parent of server/)
|
|
10
|
+
EXAMPLES_DIR = Path(__file__).parent.parent.parent / "workflows"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_example_workflows() -> List[Dict[str, Any]]:
|
|
14
|
+
"""Load all example workflow JSON files from disk."""
|
|
15
|
+
examples = []
|
|
16
|
+
if not EXAMPLES_DIR.exists():
|
|
17
|
+
logger.warning(f"Examples directory not found: {EXAMPLES_DIR}")
|
|
18
|
+
return examples
|
|
19
|
+
|
|
20
|
+
for file in sorted(EXAMPLES_DIR.glob("*.json")):
|
|
21
|
+
try:
|
|
22
|
+
with open(file, encoding="utf-8") as f:
|
|
23
|
+
workflow = json.load(f)
|
|
24
|
+
workflow["_filename"] = file.name # Track source
|
|
25
|
+
examples.append(workflow)
|
|
26
|
+
logger.debug(f"Loaded example: {file.name}")
|
|
27
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
28
|
+
logger.error(f"Failed to load {file}: {e}")
|
|
29
|
+
|
|
30
|
+
return examples
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def import_examples_for_user(database) -> int:
|
|
34
|
+
"""Import all examples using existing database.save_workflow().
|
|
35
|
+
|
|
36
|
+
Returns count of workflows imported.
|
|
37
|
+
"""
|
|
38
|
+
examples = get_example_workflows()
|
|
39
|
+
imported = 0
|
|
40
|
+
|
|
41
|
+
for example in examples:
|
|
42
|
+
# Use ID from JSON, prefixed with 'example_' for clarity
|
|
43
|
+
workflow_id = f"example_{example.get('id', 'unknown')}"
|
|
44
|
+
|
|
45
|
+
# Reuse existing save_workflow method
|
|
46
|
+
success = await database.save_workflow(
|
|
47
|
+
workflow_id=workflow_id,
|
|
48
|
+
name=example.get("name", "Example Workflow"),
|
|
49
|
+
description=example.get("description"),
|
|
50
|
+
data={
|
|
51
|
+
"nodes": example.get("nodes", []),
|
|
52
|
+
"edges": example.get("edges", [])
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if success:
|
|
57
|
+
imported += 1
|
|
58
|
+
logger.info(f"Imported example: {example.get('name')}")
|
|
59
|
+
|
|
60
|
+
return imported
|
|
@@ -804,6 +804,7 @@ class WorkflowExecutor:
|
|
|
804
804
|
|
|
805
805
|
# Build execution context for node handler
|
|
806
806
|
# workflow_id is included for per-workflow status scoping (n8n pattern)
|
|
807
|
+
logger.info(f"[Executor] Building context for {node.node_id}, ctx.outputs keys: {list(ctx.outputs.keys())}")
|
|
807
808
|
exec_context = {
|
|
808
809
|
"nodes": ctx.nodes,
|
|
809
810
|
"edges": ctx.edges,
|
|
@@ -813,6 +814,7 @@ class WorkflowExecutor:
|
|
|
813
814
|
"start_time": node.started_at,
|
|
814
815
|
"outputs": ctx.outputs, # Previous node outputs
|
|
815
816
|
}
|
|
817
|
+
logger.info(f"[Executor] exec_context['outputs'] keys: {list(exec_context['outputs'].keys())}")
|
|
816
818
|
|
|
817
819
|
# Call the actual node executor
|
|
818
820
|
result = await self.node_executor(
|
|
@@ -13,6 +13,10 @@ from datetime import datetime
|
|
|
13
13
|
from enum import Enum
|
|
14
14
|
from typing import Dict, Any, List, Optional
|
|
15
15
|
|
|
16
|
+
from core.logging import get_logger
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
class TaskStatus(str, Enum):
|
|
18
22
|
"""Task execution states (Conductor-style lifecycle).
|
|
@@ -361,6 +365,8 @@ class ExecutionContext:
|
|
|
361
365
|
if node.get("_pre_executed"):
|
|
362
366
|
# Mark as COMPLETED with trigger output
|
|
363
367
|
trigger_output = node.get("_trigger_output", {})
|
|
368
|
+
logger.info(f"[ExecutionContext] Pre-executed node found: {node_id} (type={node_type})")
|
|
369
|
+
logger.info(f"[ExecutionContext] Trigger output keys: {list(trigger_output.keys()) if trigger_output else 'empty'}")
|
|
364
370
|
node_exec = NodeExecution(
|
|
365
371
|
node_id=node_id,
|
|
366
372
|
node_type=node_type,
|
|
@@ -369,6 +375,7 @@ class ExecutionContext:
|
|
|
369
375
|
completed_at=time.time(),
|
|
370
376
|
)
|
|
371
377
|
ctx.outputs[node_id] = trigger_output
|
|
378
|
+
logger.info(f"[ExecutionContext] Set ctx.outputs[{node_id}] = trigger_output")
|
|
372
379
|
ctx.checkpoints.append(node_id)
|
|
373
380
|
else:
|
|
374
381
|
node_exec = NodeExecution(
|
|
@@ -378,6 +385,7 @@ class ExecutionContext:
|
|
|
378
385
|
|
|
379
386
|
ctx.node_executions[node_id] = node_exec
|
|
380
387
|
|
|
388
|
+
logger.info(f"[ExecutionContext] Created context with outputs: {list(ctx.outputs.keys())}")
|
|
381
389
|
return ctx
|
|
382
390
|
|
|
383
391
|
def get_node_status(self, node_id: str) -> Optional[TaskStatus]:
|
|
@@ -84,6 +84,7 @@ from .document import (
|
|
|
84
84
|
# Tool execution handlers (for AI Agent tool calling)
|
|
85
85
|
from .tools import (
|
|
86
86
|
execute_tool,
|
|
87
|
+
handle_task_manager,
|
|
87
88
|
)
|
|
88
89
|
|
|
89
90
|
__all__ = [
|
|
@@ -129,4 +130,5 @@ __all__ = [
|
|
|
129
130
|
'handle_vector_store',
|
|
130
131
|
# Tools
|
|
131
132
|
'execute_tool',
|
|
133
|
+
'handle_task_manager',
|
|
132
134
|
]
|