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
|
@@ -9,7 +9,8 @@ import math
|
|
|
9
9
|
import json
|
|
10
10
|
import asyncio
|
|
11
11
|
import uuid
|
|
12
|
-
|
|
12
|
+
import hashlib
|
|
13
|
+
from typing import Dict, Any, Optional, List, Tuple, TYPE_CHECKING
|
|
13
14
|
|
|
14
15
|
from core.logging import get_logger
|
|
15
16
|
from constants import ANDROID_SERVICE_NODE_TYPES
|
|
@@ -23,6 +24,13 @@ logger = get_logger(__name__)
|
|
|
23
24
|
# Track running delegated tasks for status checking
|
|
24
25
|
_delegated_tasks: Dict[str, asyncio.Task] = {}
|
|
25
26
|
|
|
27
|
+
# In-memory cache of delegation results (fast path, survives task cleanup)
|
|
28
|
+
# Follows Celery AsyncResult / Ray ObjectRef pattern
|
|
29
|
+
_delegation_results: Dict[str, Dict[str, Any]] = {}
|
|
30
|
+
|
|
31
|
+
# Track active delegations to prevent duplicate calls: (parent_node_id, child_node_id, task_hash) -> task_id
|
|
32
|
+
_active_delegations: Dict[Tuple[str, str, str], str] = {}
|
|
33
|
+
|
|
26
34
|
|
|
27
35
|
async def execute_tool(tool_name: str, tool_args: Dict[str, Any],
|
|
28
36
|
config: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -115,6 +123,15 @@ async def execute_tool(tool_name: str, tool_args: Dict[str, Any],
|
|
|
115
123
|
if node_type == 'gmaps_nearby_places':
|
|
116
124
|
return await _execute_nearby_places(tool_args, config.get('parameters', {}))
|
|
117
125
|
|
|
126
|
+
# Task Manager (dual-purpose: AI tool + workflow node)
|
|
127
|
+
if node_type == 'taskManager':
|
|
128
|
+
return await _execute_task_manager(tool_args, config)
|
|
129
|
+
|
|
130
|
+
# Built-in: Check delegated task results
|
|
131
|
+
# Auto-injected when parent has delegation tools
|
|
132
|
+
if node_type == '_builtin_check_delegated_tasks':
|
|
133
|
+
return await _execute_check_delegated_tasks(tool_args, config)
|
|
134
|
+
|
|
118
135
|
# AI Agent delegation (fire-and-forget async delegation)
|
|
119
136
|
# Includes specialized agents: android_agent, coding_agent, web_agent, task_agent, social_agent
|
|
120
137
|
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'):
|
|
@@ -1158,9 +1175,34 @@ async def _execute_delegated_agent(args: Dict[str, Any],
|
|
|
1158
1175
|
"hint": "Ensure nodes/edges are passed to tool config"
|
|
1159
1176
|
}
|
|
1160
1177
|
|
|
1178
|
+
# Get parent node ID for duplicate tracking
|
|
1179
|
+
parent_node_id = config.get('parent_node_id', '')
|
|
1180
|
+
|
|
1181
|
+
# Generate hash of task to detect duplicate delegation attempts
|
|
1182
|
+
task_hash = hashlib.md5(f"{task_description}:{task_context}".encode()).hexdigest()[:16]
|
|
1183
|
+
delegation_key = (parent_node_id, node_id, task_hash)
|
|
1184
|
+
|
|
1185
|
+
# Check for duplicate delegation (prevents LLM from calling same delegation twice)
|
|
1186
|
+
existing_task_id = _active_delegations.get(delegation_key)
|
|
1187
|
+
if existing_task_id:
|
|
1188
|
+
logger.warning(f"[Delegated Agent] Duplicate delegation detected: task_hash={task_hash}, existing_task_id={existing_task_id}")
|
|
1189
|
+
return {
|
|
1190
|
+
"success": True,
|
|
1191
|
+
"status": "ALREADY_DELEGATED",
|
|
1192
|
+
"task_id": existing_task_id,
|
|
1193
|
+
"agent_name": config.get('parameters', {}).get('label', node_type),
|
|
1194
|
+
"result": (
|
|
1195
|
+
f"This task was ALREADY delegated (task_id: {existing_task_id}). "
|
|
1196
|
+
f"Do NOT call this tool again. Use 'check_delegated_tasks' to check status."
|
|
1197
|
+
),
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1161
1200
|
# Generate unique task ID
|
|
1162
1201
|
task_id = f"delegated_{node_id}_{uuid.uuid4().hex[:8]}"
|
|
1163
1202
|
|
|
1203
|
+
# Register this delegation to prevent duplicates
|
|
1204
|
+
_active_delegations[delegation_key] = task_id
|
|
1205
|
+
|
|
1164
1206
|
# Get child agent parameters from database
|
|
1165
1207
|
child_params = await database.get_node_parameters(node_id) or {}
|
|
1166
1208
|
|
|
@@ -1233,8 +1275,69 @@ async def _execute_delegated_agent(args: Dict[str, Any],
|
|
|
1233
1275
|
|
|
1234
1276
|
logger.info(f"[Delegated Agent] Task {task_id} completed: success={result.get('success')}")
|
|
1235
1277
|
|
|
1278
|
+
# Check if child agent actually succeeded
|
|
1279
|
+
if not result.get('success'):
|
|
1280
|
+
# Child agent returned failure - treat as error
|
|
1281
|
+
error_msg = result.get('error', 'Child agent returned failure')
|
|
1282
|
+
logger.warning(f"[Delegated Agent] Task {task_id} returned success=False: {error_msg}")
|
|
1283
|
+
|
|
1284
|
+
await broadcaster.update_node_status(
|
|
1285
|
+
node_id,
|
|
1286
|
+
"error",
|
|
1287
|
+
{
|
|
1288
|
+
"phase": "delegated_error",
|
|
1289
|
+
"task_id": task_id,
|
|
1290
|
+
"error": error_msg
|
|
1291
|
+
},
|
|
1292
|
+
workflow_id=workflow_id
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
# Cache error for parent retrieval
|
|
1296
|
+
_delegation_results[task_id] = {
|
|
1297
|
+
"task_id": task_id,
|
|
1298
|
+
"status": "error",
|
|
1299
|
+
"agent_name": agent_label,
|
|
1300
|
+
"agent_node_id": node_id,
|
|
1301
|
+
"result": None,
|
|
1302
|
+
"error": error_msg,
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
# Persist error to DB
|
|
1306
|
+
if database:
|
|
1307
|
+
await database.save_node_output(
|
|
1308
|
+
node_id=node_id,
|
|
1309
|
+
session_id=f"delegation_{task_id}",
|
|
1310
|
+
output_name="delegation_result",
|
|
1311
|
+
data={
|
|
1312
|
+
"task_id": task_id,
|
|
1313
|
+
"parent_node_id": config.get('parent_node_id', ''),
|
|
1314
|
+
"agent_name": agent_label,
|
|
1315
|
+
"status": "error",
|
|
1316
|
+
"error": error_msg,
|
|
1317
|
+
}
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
# Dispatch error event for trigger nodes
|
|
1321
|
+
await broadcaster.send_custom_event('task_completed', {
|
|
1322
|
+
'task_id': task_id,
|
|
1323
|
+
'status': 'error',
|
|
1324
|
+
'agent_name': agent_label,
|
|
1325
|
+
'agent_node_id': node_id,
|
|
1326
|
+
'parent_node_id': config.get('parent_node_id', ''),
|
|
1327
|
+
'error': error_msg,
|
|
1328
|
+
'workflow_id': workflow_id,
|
|
1329
|
+
})
|
|
1330
|
+
|
|
1331
|
+
return result
|
|
1332
|
+
|
|
1333
|
+
# Success case - extract response properly
|
|
1334
|
+
response_text = result.get('result', {}).get('response', '')
|
|
1335
|
+
if not response_text:
|
|
1336
|
+
# Fallback: try to stringify the result dict
|
|
1337
|
+
response_text = str(result.get('result', '')) if result.get('result') else 'No response generated'
|
|
1338
|
+
|
|
1236
1339
|
# Broadcast completion
|
|
1237
|
-
response_preview =
|
|
1340
|
+
response_preview = response_text[:200] if response_text else ''
|
|
1238
1341
|
await broadcaster.update_node_status(
|
|
1239
1342
|
node_id,
|
|
1240
1343
|
"success",
|
|
@@ -1246,6 +1349,42 @@ async def _execute_delegated_agent(args: Dict[str, Any],
|
|
|
1246
1349
|
workflow_id=workflow_id
|
|
1247
1350
|
)
|
|
1248
1351
|
|
|
1352
|
+
# Cache result for parent retrieval (Layer 2: in-memory)
|
|
1353
|
+
_delegation_results[task_id] = {
|
|
1354
|
+
"task_id": task_id,
|
|
1355
|
+
"status": "completed",
|
|
1356
|
+
"agent_name": agent_label,
|
|
1357
|
+
"agent_node_id": node_id,
|
|
1358
|
+
"result": response_text,
|
|
1359
|
+
"error": None,
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
# Persist to DB (Layer 3: cross-restart via existing NodeOutput)
|
|
1363
|
+
if database:
|
|
1364
|
+
await database.save_node_output(
|
|
1365
|
+
node_id=node_id,
|
|
1366
|
+
session_id=f"delegation_{task_id}",
|
|
1367
|
+
output_name="delegation_result",
|
|
1368
|
+
data={
|
|
1369
|
+
"task_id": task_id,
|
|
1370
|
+
"parent_node_id": config.get('parent_node_id', ''),
|
|
1371
|
+
"agent_name": agent_label,
|
|
1372
|
+
"status": "completed",
|
|
1373
|
+
"result": response_text,
|
|
1374
|
+
}
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
# Dispatch task_completed event for trigger nodes
|
|
1378
|
+
await broadcaster.send_custom_event('task_completed', {
|
|
1379
|
+
'task_id': task_id,
|
|
1380
|
+
'status': 'completed',
|
|
1381
|
+
'agent_name': agent_label,
|
|
1382
|
+
'agent_node_id': node_id,
|
|
1383
|
+
'parent_node_id': config.get('parent_node_id', ''),
|
|
1384
|
+
'result': response_text,
|
|
1385
|
+
'workflow_id': workflow_id,
|
|
1386
|
+
})
|
|
1387
|
+
|
|
1249
1388
|
return result
|
|
1250
1389
|
|
|
1251
1390
|
except Exception as e:
|
|
@@ -1260,11 +1399,50 @@ async def _execute_delegated_agent(args: Dict[str, Any],
|
|
|
1260
1399
|
},
|
|
1261
1400
|
workflow_id=workflow_id
|
|
1262
1401
|
)
|
|
1402
|
+
|
|
1403
|
+
# Cache error for parent retrieval (Layer 2: in-memory)
|
|
1404
|
+
_delegation_results[task_id] = {
|
|
1405
|
+
"task_id": task_id,
|
|
1406
|
+
"status": "error",
|
|
1407
|
+
"agent_name": agent_label,
|
|
1408
|
+
"agent_node_id": node_id,
|
|
1409
|
+
"result": None,
|
|
1410
|
+
"error": str(e),
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
# Persist to DB (Layer 3: cross-restart)
|
|
1414
|
+
if database:
|
|
1415
|
+
await database.save_node_output(
|
|
1416
|
+
node_id=node_id,
|
|
1417
|
+
session_id=f"delegation_{task_id}",
|
|
1418
|
+
output_name="delegation_result",
|
|
1419
|
+
data={
|
|
1420
|
+
"task_id": task_id,
|
|
1421
|
+
"parent_node_id": config.get('parent_node_id', ''),
|
|
1422
|
+
"agent_name": agent_label,
|
|
1423
|
+
"status": "error",
|
|
1424
|
+
"error": str(e),
|
|
1425
|
+
}
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
# Dispatch task_completed event for trigger nodes (error case)
|
|
1429
|
+
await broadcaster.send_custom_event('task_completed', {
|
|
1430
|
+
'task_id': task_id,
|
|
1431
|
+
'status': 'error',
|
|
1432
|
+
'agent_name': agent_label,
|
|
1433
|
+
'agent_node_id': node_id,
|
|
1434
|
+
'parent_node_id': config.get('parent_node_id', ''),
|
|
1435
|
+
'error': str(e),
|
|
1436
|
+
'workflow_id': workflow_id,
|
|
1437
|
+
})
|
|
1438
|
+
|
|
1263
1439
|
return {"success": False, "error": str(e)}
|
|
1264
1440
|
|
|
1265
1441
|
finally:
|
|
1266
1442
|
# Cleanup task reference
|
|
1267
1443
|
_delegated_tasks.pop(task_id, None)
|
|
1444
|
+
# Cleanup delegation tracking (allows re-delegation after completion)
|
|
1445
|
+
_active_delegations.pop(delegation_key, None)
|
|
1268
1446
|
|
|
1269
1447
|
# Spawn as background task (fire-and-forget)
|
|
1270
1448
|
task = asyncio.create_task(run_child_agent())
|
|
@@ -1277,24 +1455,277 @@ async def _execute_delegated_agent(args: Dict[str, Any],
|
|
|
1277
1455
|
"task_id": task_id,
|
|
1278
1456
|
"agent_node_id": node_id,
|
|
1279
1457
|
"agent_name": agent_label,
|
|
1280
|
-
"message":
|
|
1458
|
+
"message": (
|
|
1459
|
+
f"SUCCESS: Task delegated to '{agent_label}' (task_id: {task_id}). "
|
|
1460
|
+
f"Agent is now working INDEPENDENTLY in the background. "
|
|
1461
|
+
f"IMPORTANT: Delegation is COMPLETE. Do NOT call this tool again for this task. "
|
|
1462
|
+
f"To check results later, use 'check_delegated_tasks' with task_id='{task_id}'."
|
|
1463
|
+
),
|
|
1281
1464
|
}
|
|
1282
1465
|
|
|
1283
1466
|
|
|
1284
|
-
def get_delegated_task_status(
|
|
1285
|
-
|
|
1467
|
+
async def get_delegated_task_status(task_ids: Optional[List[str]] = None,
|
|
1468
|
+
database=None) -> Dict[str, Any]:
|
|
1469
|
+
"""Check status and retrieve results of delegated tasks.
|
|
1470
|
+
|
|
1471
|
+
3-layer lookup: live tasks -> memory cache -> DB (NodeOutput).
|
|
1472
|
+
Follows Celery AsyncResult / Ray ObjectRef pattern.
|
|
1286
1473
|
|
|
1287
1474
|
Args:
|
|
1288
|
-
|
|
1475
|
+
task_ids: Optional list of specific task IDs to check. If None, returns all known tasks.
|
|
1476
|
+
database: Database instance for Layer 3 (SQLite) lookup.
|
|
1289
1477
|
|
|
1290
1478
|
Returns:
|
|
1291
|
-
|
|
1479
|
+
Dict with 'tasks' list containing status and results for each task.
|
|
1292
1480
|
"""
|
|
1293
|
-
|
|
1481
|
+
if not task_ids:
|
|
1482
|
+
# Return all known from memory
|
|
1483
|
+
task_ids = list(set(
|
|
1484
|
+
list(_delegated_tasks.keys()) + list(_delegation_results.keys())
|
|
1485
|
+
))
|
|
1486
|
+
|
|
1487
|
+
tasks = []
|
|
1488
|
+
db_lookup_ids = []
|
|
1489
|
+
|
|
1490
|
+
for tid in task_ids:
|
|
1491
|
+
# Layer 1: Live asyncio.Task (still running or just finished)
|
|
1492
|
+
live_task = _delegated_tasks.get(tid)
|
|
1493
|
+
if live_task is not None:
|
|
1494
|
+
if not live_task.done():
|
|
1495
|
+
tasks.append({"task_id": tid, "status": "running"})
|
|
1496
|
+
else:
|
|
1497
|
+
# Task finished -- extract result via task.result()
|
|
1498
|
+
try:
|
|
1499
|
+
result = live_task.result()
|
|
1500
|
+
response = result.get('result', {}).get('response', str(result.get('result', '')))
|
|
1501
|
+
tasks.append({"task_id": tid, "status": "completed", "result": response})
|
|
1502
|
+
except Exception as e:
|
|
1503
|
+
tasks.append({"task_id": tid, "status": "error", "error": str(e)})
|
|
1504
|
+
continue
|
|
1505
|
+
|
|
1506
|
+
# Layer 2: In-memory result cache
|
|
1507
|
+
cached = _delegation_results.get(tid)
|
|
1508
|
+
if cached:
|
|
1509
|
+
tasks.append(cached)
|
|
1510
|
+
continue
|
|
1511
|
+
|
|
1512
|
+
# Layer 3: Need DB lookup
|
|
1513
|
+
db_lookup_ids.append(tid)
|
|
1514
|
+
|
|
1515
|
+
# DB fallback for results not in memory (cross-restart)
|
|
1516
|
+
if db_lookup_ids and database:
|
|
1517
|
+
for tid in list(db_lookup_ids):
|
|
1518
|
+
db_result = await database.get_node_output_by_session(
|
|
1519
|
+
session_id=f"delegation_{tid}",
|
|
1520
|
+
output_name="delegation_result"
|
|
1521
|
+
)
|
|
1522
|
+
if db_result:
|
|
1523
|
+
data = db_result.get('data', {})
|
|
1524
|
+
result_data = data.get("result", {})
|
|
1525
|
+
response_text = result_data.get("response", str(result_data)) if isinstance(result_data, dict) else str(result_data)
|
|
1526
|
+
tasks.append({
|
|
1527
|
+
"task_id": tid,
|
|
1528
|
+
"status": data.get("status", "completed"),
|
|
1529
|
+
"agent_name": data.get("agent_name", ""),
|
|
1530
|
+
"result": response_text,
|
|
1531
|
+
"error": data.get("error"),
|
|
1532
|
+
})
|
|
1533
|
+
db_lookup_ids.remove(tid)
|
|
1534
|
+
|
|
1535
|
+
# Remaining IDs not found anywhere
|
|
1536
|
+
for tid in db_lookup_ids:
|
|
1537
|
+
tasks.append({"task_id": tid, "status": "not_found"})
|
|
1538
|
+
|
|
1539
|
+
return {"tasks": tasks}
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
async def _execute_check_delegated_tasks(args: Dict[str, Any],
|
|
1543
|
+
config: Dict[str, Any]) -> Dict[str, Any]:
|
|
1544
|
+
"""LLM-callable tool: check on delegated child agents.
|
|
1545
|
+
|
|
1546
|
+
Returns status and results for previously delegated tasks.
|
|
1547
|
+
Follows Celery AsyncResult / Ray ObjectRef patterns.
|
|
1548
|
+
"""
|
|
1549
|
+
task_ids = args.get('task_ids')
|
|
1550
|
+
database = config.get('database')
|
|
1551
|
+
result = await get_delegated_task_status(task_ids=task_ids, database=database)
|
|
1552
|
+
|
|
1553
|
+
formatted = []
|
|
1554
|
+
for task in result.get("tasks", []):
|
|
1555
|
+
entry = {
|
|
1556
|
+
"task_id": task.get("task_id"),
|
|
1557
|
+
"status": task.get("status"),
|
|
1558
|
+
"agent_name": task.get("agent_name"),
|
|
1559
|
+
}
|
|
1560
|
+
if task.get("status") == "completed":
|
|
1561
|
+
text = str(task.get("result", ""))
|
|
1562
|
+
entry["result"] = text[:4000] + "... [truncated]" if len(text) > 4000 else text
|
|
1563
|
+
elif task.get("status") == "error":
|
|
1564
|
+
entry["error"] = task.get("error")
|
|
1565
|
+
elif task.get("status") == "running":
|
|
1566
|
+
entry["message"] = f"Agent '{task.get('agent_name', 'unknown')}' is still working"
|
|
1567
|
+
formatted.append(entry)
|
|
1294
1568
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1569
|
+
return {
|
|
1570
|
+
"total_tasks": len(formatted),
|
|
1571
|
+
"completed": sum(1 for t in formatted if t.get("status") == "completed"),
|
|
1572
|
+
"running": sum(1 for t in formatted if t.get("status") == "running"),
|
|
1573
|
+
"errors": sum(1 for t in formatted if t.get("status") == "error"),
|
|
1574
|
+
"tasks": formatted,
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
async def _execute_task_manager(
|
|
1579
|
+
tool_args: Dict[str, Any],
|
|
1580
|
+
config: Dict[str, Any]
|
|
1581
|
+
) -> Dict[str, Any]:
|
|
1582
|
+
"""Execute task manager operations.
|
|
1583
|
+
|
|
1584
|
+
Dual-purpose: works as AI tool (LLM fills args) or workflow node (uses params).
|
|
1585
|
+
|
|
1586
|
+
Operations:
|
|
1587
|
+
- list_tasks: List all active and completed delegated tasks
|
|
1588
|
+
- get_task: Get status details for a specific task
|
|
1589
|
+
- mark_done: Remove a task from active tracking
|
|
1590
|
+
|
|
1591
|
+
Args:
|
|
1592
|
+
tool_args: Arguments from LLM tool call (operation, task_id, status_filter)
|
|
1593
|
+
config: Tool configuration with node parameters
|
|
1594
|
+
|
|
1595
|
+
Returns:
|
|
1596
|
+
Dict with operation results
|
|
1597
|
+
"""
|
|
1598
|
+
# Merge tool_args with node parameters (tool_args takes precedence)
|
|
1599
|
+
params = config.get('parameters', {})
|
|
1600
|
+
operation = tool_args.get('operation') or params.get('operation', 'list_tasks')
|
|
1601
|
+
task_id = tool_args.get('task_id') or params.get('task_id')
|
|
1602
|
+
status_filter = tool_args.get('status_filter') or params.get('status_filter')
|
|
1603
|
+
database = config.get('database')
|
|
1604
|
+
|
|
1605
|
+
logger.debug(f"[TaskManager] Operation: {operation}, task_id: {task_id}, filter: {status_filter}")
|
|
1606
|
+
|
|
1607
|
+
if operation == 'list_tasks':
|
|
1608
|
+
# Collect all tasks from _delegated_tasks and _delegation_results
|
|
1609
|
+
tasks = []
|
|
1610
|
+
|
|
1611
|
+
# Active tasks from asyncio.Task tracking
|
|
1612
|
+
for tid, task in _delegated_tasks.items():
|
|
1613
|
+
if task.done():
|
|
1614
|
+
try:
|
|
1615
|
+
if task.cancelled():
|
|
1616
|
+
status = 'cancelled'
|
|
1617
|
+
elif task.exception():
|
|
1618
|
+
status = 'error'
|
|
1619
|
+
else:
|
|
1620
|
+
status = 'completed'
|
|
1621
|
+
except Exception:
|
|
1622
|
+
status = 'completed'
|
|
1623
|
+
else:
|
|
1624
|
+
status = 'running'
|
|
1625
|
+
|
|
1626
|
+
tasks.append({
|
|
1627
|
+
'task_id': tid,
|
|
1628
|
+
'status': status,
|
|
1629
|
+
'active': True
|
|
1630
|
+
})
|
|
1631
|
+
|
|
1632
|
+
# Completed tasks from in-memory cache
|
|
1633
|
+
for tid, result in _delegation_results.items():
|
|
1634
|
+
if tid not in [t['task_id'] for t in tasks]:
|
|
1635
|
+
tasks.append({
|
|
1636
|
+
'task_id': tid,
|
|
1637
|
+
'status': result.get('status', 'completed'),
|
|
1638
|
+
'agent_name': result.get('agent_name'),
|
|
1639
|
+
'result_summary': str(result.get('result', ''))[:200],
|
|
1640
|
+
'active': False
|
|
1641
|
+
})
|
|
1642
|
+
|
|
1643
|
+
# Apply status filter
|
|
1644
|
+
if status_filter:
|
|
1645
|
+
tasks = [t for t in tasks if t.get('status') == status_filter]
|
|
1646
|
+
|
|
1647
|
+
return {
|
|
1648
|
+
'success': True,
|
|
1649
|
+
'operation': 'list_tasks',
|
|
1650
|
+
'tasks': tasks,
|
|
1651
|
+
'count': len(tasks),
|
|
1652
|
+
'running': sum(1 for t in tasks if t.get('status') == 'running'),
|
|
1653
|
+
'completed': sum(1 for t in tasks if t.get('status') == 'completed'),
|
|
1654
|
+
'errors': sum(1 for t in tasks if t.get('status') == 'error')
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
elif operation == 'get_task':
|
|
1658
|
+
if not task_id:
|
|
1659
|
+
return {'success': False, 'error': 'task_id is required for get_task operation'}
|
|
1660
|
+
|
|
1661
|
+
# Use existing get_delegated_task_status for detailed lookup
|
|
1662
|
+
result = await get_delegated_task_status(task_ids=[task_id], database=database)
|
|
1663
|
+
tasks = result.get('tasks', [])
|
|
1664
|
+
|
|
1665
|
+
if not tasks:
|
|
1666
|
+
return {
|
|
1667
|
+
'success': False,
|
|
1668
|
+
'error': f'Task {task_id} not found',
|
|
1669
|
+
'task_id': task_id
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
task_info = tasks[0]
|
|
1673
|
+
return {
|
|
1674
|
+
'success': True,
|
|
1675
|
+
'operation': 'get_task',
|
|
1676
|
+
'task_id': task_id,
|
|
1677
|
+
'status': task_info.get('status'),
|
|
1678
|
+
'agent_name': task_info.get('agent_name'),
|
|
1679
|
+
'result': task_info.get('result'),
|
|
1680
|
+
'error': task_info.get('error')
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
elif operation == 'mark_done':
|
|
1684
|
+
if not task_id:
|
|
1685
|
+
return {'success': False, 'error': 'task_id is required for mark_done operation'}
|
|
1686
|
+
|
|
1687
|
+
# Remove from active tracking
|
|
1688
|
+
removed = False
|
|
1689
|
+
if task_id in _delegated_tasks:
|
|
1690
|
+
del _delegated_tasks[task_id]
|
|
1691
|
+
removed = True
|
|
1692
|
+
if task_id in _delegation_results:
|
|
1693
|
+
del _delegation_results[task_id]
|
|
1694
|
+
removed = True
|
|
1695
|
+
|
|
1696
|
+
return {
|
|
1697
|
+
'success': True,
|
|
1698
|
+
'operation': 'mark_done',
|
|
1699
|
+
'task_id': task_id,
|
|
1700
|
+
'removed': removed,
|
|
1701
|
+
'message': f'Task {task_id} marked as done and removed from tracking' if removed else f'Task {task_id} was not in active tracking'
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
return {'success': False, 'error': f'Unknown operation: {operation}'}
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
async def handle_task_manager(
|
|
1708
|
+
node_id: str,
|
|
1709
|
+
node_type: str,
|
|
1710
|
+
parameters: Dict[str, Any],
|
|
1711
|
+
context: Dict[str, Any]
|
|
1712
|
+
) -> Dict[str, Any]:
|
|
1713
|
+
"""Handle taskManager as workflow node.
|
|
1714
|
+
|
|
1715
|
+
Wrapper for _execute_task_manager that conforms to the standard
|
|
1716
|
+
workflow node handler signature.
|
|
1717
|
+
|
|
1718
|
+
Args:
|
|
1719
|
+
node_id: The node ID
|
|
1720
|
+
node_type: Should be 'taskManager'
|
|
1721
|
+
parameters: Node parameters (operation, task_id, status_filter)
|
|
1722
|
+
context: Execution context
|
|
1723
|
+
|
|
1724
|
+
Returns:
|
|
1725
|
+
Task manager operation results
|
|
1726
|
+
"""
|
|
1727
|
+
config = {
|
|
1728
|
+
'parameters': parameters,
|
|
1729
|
+
'database': context.get('database')
|
|
1730
|
+
}
|
|
1731
|
+
return await _execute_task_manager({}, config)
|
|
@@ -35,6 +35,7 @@ from services.handlers import (
|
|
|
35
35
|
handle_social_receive, handle_social_send,
|
|
36
36
|
handle_http_scraper, handle_file_downloader, handle_document_parser,
|
|
37
37
|
handle_text_chunker, handle_embedding_generator, handle_vector_store,
|
|
38
|
+
handle_task_manager,
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
if TYPE_CHECKING:
|
|
@@ -145,6 +146,8 @@ class NodeExecutor:
|
|
|
145
146
|
'textChunker': handle_text_chunker,
|
|
146
147
|
'embeddingGenerator': handle_embedding_generator,
|
|
147
148
|
'vectorStore': handle_vector_store,
|
|
149
|
+
# Task management
|
|
150
|
+
'taskManager': handle_task_manager,
|
|
148
151
|
# Note: 'console' handled in _dispatch with connected_outputs
|
|
149
152
|
}
|
|
150
153
|
|
|
@@ -208,6 +208,9 @@ class NodeExecutionActivities:
|
|
|
208
208
|
"edges": context.get("edges", []),
|
|
209
209
|
"session_id": context.get("session_id", "default"),
|
|
210
210
|
"workflow_id": context.get("workflow_id"),
|
|
211
|
+
# CRITICAL: Pass upstream node outputs for downstream nodes to access
|
|
212
|
+
# This enables taskTrigger -> chatAgent data flow via input-task handle
|
|
213
|
+
"outputs": context.get("inputs", {}),
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
print(f"[Activity] WebSocket execute for {node_id}")
|
|
@@ -142,6 +142,7 @@ class WorkflowService:
|
|
|
142
142
|
session_id: str = "default",
|
|
143
143
|
execution_id: str = None,
|
|
144
144
|
workflow_id: str = None,
|
|
145
|
+
outputs: Dict[str, Any] = None,
|
|
145
146
|
) -> Dict[str, Any]:
|
|
146
147
|
"""Execute a single workflow node."""
|
|
147
148
|
context = {
|
|
@@ -151,6 +152,7 @@ class WorkflowService:
|
|
|
151
152
|
"execution_id": execution_id,
|
|
152
153
|
"workflow_id": workflow_id, # For per-workflow status scoping (n8n pattern)
|
|
153
154
|
"get_output_fn": self.get_node_output,
|
|
155
|
+
"outputs": outputs or {}, # Upstream node outputs for data flow (e.g., taskTrigger -> chatAgent)
|
|
154
156
|
}
|
|
155
157
|
return await self._node_executor.execute(
|
|
156
158
|
node_id=node_id,
|