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.
Files changed (72) hide show
  1. package/.env.template +16 -0
  2. package/client/package.json +1 -1
  3. package/client/src/Dashboard.tsx +3 -3
  4. package/client/src/components/AIAgentNode.tsx +24 -12
  5. package/client/src/components/OutputPanel.tsx +3 -2
  6. package/client/src/components/parameterPanel/InputSection.tsx +16 -3
  7. package/client/src/nodeDefinitions/aiAgentNodes.ts +12 -0
  8. package/client/src/nodeDefinitions/specializedAgentNodes.ts +68 -320
  9. package/client/src/nodeDefinitions/toolNodes.ts +87 -1
  10. package/client/src/nodeDefinitions/workflowNodes.ts +55 -1
  11. package/package.json +12 -3
  12. package/scripts/daemon.js +427 -0
  13. package/scripts/start.js +7 -1
  14. package/scripts/sync-version.js +108 -0
  15. package/server/Dockerfile +6 -7
  16. package/server/constants.py +2 -0
  17. package/server/core/cleanup.py +123 -0
  18. package/server/core/config.py +16 -0
  19. package/server/core/database.py +92 -1
  20. package/server/core/health.py +121 -0
  21. package/server/examples/__init__.py +1 -0
  22. package/server/gunicorn.conf.py +46 -0
  23. package/server/main.py +38 -3
  24. package/server/models/database.py +1 -0
  25. package/server/models/nodes.py +18 -2
  26. package/server/requirements-docker.txt +86 -0
  27. package/server/routers/database.py +16 -0
  28. package/server/routers/websocket.py +6 -5
  29. package/server/services/ai.py +115 -14
  30. package/server/services/auth.py +6 -1
  31. package/server/services/deployment/manager.py +14 -0
  32. package/server/services/event_waiter.py +55 -0
  33. package/server/services/example_loader.py +60 -0
  34. package/server/services/execution/executor.py +2 -0
  35. package/server/services/execution/models.py +8 -0
  36. package/server/services/handlers/__init__.py +2 -0
  37. package/server/services/handlers/ai.py +164 -11
  38. package/server/services/handlers/document.py +13 -4
  39. package/server/services/handlers/tools.py +445 -14
  40. package/server/services/node_executor.py +3 -0
  41. package/server/services/temporal/activities.py +3 -0
  42. package/server/services/workflow.py +2 -0
  43. package/server/skills/android_agent/app-launcher-skill/SKILL.md +137 -0
  44. package/server/skills/android_agent/app-list-skill/SKILL.md +148 -0
  45. package/server/skills/android_agent/audio-skill/SKILL.md +169 -0
  46. package/server/skills/android_agent/battery-skill/SKILL.md +114 -0
  47. package/server/skills/android_agent/bluetooth-skill/SKILL.md +151 -0
  48. package/server/skills/android_agent/camera-skill/SKILL.md +148 -0
  49. package/server/skills/android_agent/environmental-skill/SKILL.md +140 -0
  50. package/server/skills/android_agent/location-skill/SKILL.md +163 -0
  51. package/server/skills/android_agent/motion-skill/SKILL.md +141 -0
  52. package/server/skills/android_agent/screen-control-skill/SKILL.md +164 -0
  53. package/server/skills/android_agent/wifi-skill/SKILL.md +182 -0
  54. package/server/skills/assistant/subagent-skill/SKILL.md +205 -0
  55. package/server/skills/coding_agent/javascript-skill/SKILL.md +196 -0
  56. package/server/skills/coding_agent/python-skill/SKILL.md +165 -0
  57. package/server/skills/social_agent/whatsapp-db-skill/SKILL.md +284 -0
  58. package/server/skills/social_agent/whatsapp-send-skill/SKILL.md +180 -0
  59. package/server/skills/task_agent/cron-scheduler-skill/SKILL.md +215 -0
  60. package/server/skills/task_agent/task-manager-skill/SKILL.md +251 -0
  61. package/server/skills/task_agent/timer-skill/SKILL.md +168 -0
  62. package/server/skills/travel_agent/geocoding-skill/SKILL.md +186 -0
  63. package/server/skills/travel_agent/nearby-places-skill/SKILL.md +234 -0
  64. package/server/skills/web_agent/http-request-skill/SKILL.md +211 -0
  65. package/server/skills/android/skill/SKILL.md +0 -84
  66. package/server/skills/assistant/code-skill/SKILL.md +0 -176
  67. package/server/skills/assistant/http-skill/SKILL.md +0 -163
  68. package/server/skills/assistant/maps-skill/SKILL.md +0 -172
  69. package/server/skills/assistant/scheduler-skill/SKILL.md +0 -86
  70. package/server/skills/assistant/whatsapp-skill/SKILL.md +0 -285
  71. /package/server/skills/{android → android_agent}/personality/SKILL.md +0 -0
  72. /package/server/skills/{assistant → web_agent}/web-search-skill/SKILL.md +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Dict, Any, List, Optional, Tuple, TYPE_CHECKING
4
4
  from core.logging import get_logger
5
- from constants import ANDROID_SERVICE_NODE_TYPES
5
+ from constants import ANDROID_SERVICE_NODE_TYPES, AI_AGENT_TYPES
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from services.ai import AIService
@@ -16,14 +16,15 @@ async def _collect_agent_connections(
16
16
  context: Dict[str, Any],
17
17
  database: "Database",
18
18
  log_prefix: str = "[Agent]"
19
- ) -> Tuple[Optional[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]], Optional[Dict[str, Any]]]:
20
- """Shared logic for collecting memory, skill, tool, and input data from connected nodes.
19
+ ) -> Tuple[Optional[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]], Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
20
+ """Shared logic for collecting memory, skill, tool, input, and task data from connected nodes.
21
21
 
22
22
  Scans edges targeting the given node and collects:
23
23
  - Memory data from input-memory handle (simpleMemory nodes)
24
24
  - Skill data from input-skill handle (skill nodes)
25
25
  - Tool data from input-tools handle (tool nodes, including androidTool Sub-Node pattern)
26
26
  - Input data from input-main or input-chat handle (for auto-prompting fallback)
27
+ - Task data from input-task handle (taskTrigger nodes for delegated task results)
27
28
 
28
29
  Args:
29
30
  node_id: The agent node ID to collect connections for
@@ -32,7 +33,7 @@ async def _collect_agent_connections(
32
33
  log_prefix: Prefix for log messages (e.g., "[AI Agent]" or "[Chat Agent]")
33
34
 
34
35
  Returns:
35
- Tuple of (memory_data, skill_data, tool_data, input_data)
36
+ Tuple of (memory_data, skill_data, tool_data, input_data, task_data)
36
37
  """
37
38
  nodes = context.get('nodes')
38
39
  edges = context.get('edges')
@@ -42,11 +43,12 @@ async def _collect_agent_connections(
42
43
  skill_data: List[Dict[str, Any]] = []
43
44
  tool_data: List[Dict[str, Any]] = []
44
45
  input_data: Optional[Dict[str, Any]] = None
46
+ task_data: Optional[Dict[str, Any]] = None
45
47
 
46
48
  logger.info(f"{log_prefix} Processing node {node_id}, edges={len(edges) if edges else 0}, nodes={len(nodes) if nodes else 0}, workflow_id={workflow_id}")
47
49
 
48
50
  if not edges or not nodes:
49
- return memory_data, skill_data, tool_data, input_data
51
+ return memory_data, skill_data, tool_data, input_data, task_data
50
52
 
51
53
  # Log incoming edges for debugging
52
54
  incoming_edges = [e for e in edges if e.get('target') == node_id]
@@ -201,6 +203,45 @@ async def _collect_agent_connections(
201
203
  tool_entry['connected_services'] = connected_services
202
204
  logger.debug(f"{log_prefix} Android toolkit has {len(connected_services)} connected services")
203
205
 
206
+ # Special handling for AI Agent nodes - discover their connected tools
207
+ # This allows parent agent to know child agent's capabilities
208
+ if tool_type in AI_AGENT_TYPES:
209
+ child_tools = []
210
+
211
+ # Count edges targeting this child agent
212
+ child_incoming_edges = [e for e in edges if e.get('target') == source_node_id]
213
+ child_tool_edges = [e for e in child_incoming_edges if e.get('targetHandle') == 'input-tools']
214
+ logger.debug(f"{log_prefix} Child agent {source_node_id}: {len(child_incoming_edges)} incoming edges, {len(child_tool_edges)} input-tools edges")
215
+
216
+ # Log all incoming edge handles for debugging
217
+ if child_incoming_edges:
218
+ handles = [e.get('targetHandle', 'None') for e in child_incoming_edges]
219
+ logger.debug(f"{log_prefix} Child agent {source_node_id} incoming handles: {handles}")
220
+
221
+ # Scan edges for tools connected to this child agent's input-tools handle
222
+ for child_edge in edges:
223
+ if child_edge.get('target') != source_node_id:
224
+ continue
225
+ if child_edge.get('targetHandle') != 'input-tools':
226
+ continue
227
+
228
+ child_tool_id = child_edge.get('source')
229
+ child_tool_node = next((n for n in nodes if n.get('id') == child_tool_id), None)
230
+
231
+ logger.debug(f"{log_prefix} Child agent {source_node_id}: tool edge from {child_tool_id}, node found: {child_tool_node is not None}")
232
+
233
+ if child_tool_node:
234
+ child_tool_type = child_tool_node.get('type', '')
235
+ child_tool_label = child_tool_node.get('data', {}).get('label', child_tool_type)
236
+ child_tools.append({
237
+ 'node_type': child_tool_type,
238
+ 'label': child_tool_label
239
+ })
240
+
241
+ if child_tools:
242
+ tool_entry['child_tools'] = child_tools
243
+ logger.debug(f"{log_prefix} Child agent {source_node_id} has tools: {[t['label'] for t in child_tools]}")
244
+
204
245
  tool_data.append(tool_entry)
205
246
  logger.debug(f"{log_prefix} Connected tool: {tool_type}")
206
247
 
@@ -212,14 +253,90 @@ async def _collect_agent_connections(
212
253
  input_data = source_output
213
254
  logger.debug(f"{log_prefix} Input from {source_node.get('type')}: {list(source_output.keys()) if isinstance(source_output, dict) else type(source_output)}")
214
255
 
256
+ # Task data detection - taskTrigger nodes connected to input-task handle
257
+ # Used to receive results from delegated child agents
258
+ elif target_handle == 'input-task':
259
+ logger.info(f"{log_prefix} Found input-task edge from {source_node_id} (type={source_node.get('type')})")
260
+
261
+ # Try context.outputs first (parallel executor), then database via get_output_fn
262
+ source_output = context.get('outputs', {}).get(source_node_id)
263
+ logger.info(f"{log_prefix} Context outputs check for {source_node_id}: {source_output is not None}")
264
+
265
+ if not source_output:
266
+ # Database is source of truth - use get_output_fn to retrieve stored output
267
+ get_output_fn = context.get('get_output_fn')
268
+ session_id = context.get('session_id', 'default')
269
+ if get_output_fn:
270
+ try:
271
+ source_output = await get_output_fn(session_id, source_node_id, 'output_0')
272
+ logger.info(f"{log_prefix} DB lookup for {source_node_id}: {source_output is not None}")
273
+ except Exception as e:
274
+ logger.warning(f"{log_prefix} Failed to get output from DB: {e}")
275
+ else:
276
+ logger.warning(f"{log_prefix} No get_output_fn in context, cannot retrieve task output")
277
+
278
+ logger.info(f"{log_prefix} Source output for {source_node_id}: {source_output is not None}, type={type(source_output).__name__ if source_output else 'None'}")
279
+ if source_output:
280
+ # Handle nested result structure - taskTrigger may return {"result": {...}} or flat dict
281
+ if isinstance(source_output, dict) and 'result' in source_output and isinstance(source_output.get('result'), dict):
282
+ # Nested structure - extract inner result
283
+ task_data = source_output.get('result')
284
+ logger.info(f"{log_prefix} Extracted nested task_data from result key")
285
+ else:
286
+ task_data = source_output
287
+ logger.info(f"{log_prefix} Task completion data: task_id={task_data.get('task_id')}, status={task_data.get('status')}, agent_name={task_data.get('agent_name')}")
288
+
215
289
  # Log collection results
216
- logger.info(f"{log_prefix} Collected: {len(skill_data)} skills, {len(tool_data)} tools, memory={'yes' if memory_data else 'no'}, input={'yes' if input_data else 'no'}")
290
+ logger.info(f"{log_prefix} Collected: {len(skill_data)} skills, {len(tool_data)} tools, memory={'yes' if memory_data else 'no'}, input={'yes' if input_data else 'no'}, task={'yes' if task_data else 'no'}")
217
291
  for sd in skill_data:
218
292
  logger.debug(f"{log_prefix} Skill: type={sd.get('node_type')}, label={sd.get('label')}")
219
293
  for td in tool_data:
220
294
  logger.info(f"{log_prefix} Tool: type={td.get('node_type')}, node_id={td.get('node_id')}")
221
295
 
222
- return memory_data, skill_data, tool_data, input_data
296
+ return memory_data, skill_data, tool_data, input_data, task_data
297
+
298
+
299
+ def _format_task_context(task_data: Dict[str, Any]) -> str:
300
+ """Format task completion data as context for the agent.
301
+
302
+ Args:
303
+ task_data: Task completion data from taskTrigger node
304
+
305
+ Returns:
306
+ Formatted string to prepend to agent prompt
307
+ """
308
+ status = task_data.get('status', 'unknown')
309
+ agent_name = task_data.get('agent_name', 'Unknown Agent')
310
+ task_id = task_data.get('task_id', '')
311
+
312
+ if status == 'completed':
313
+ result = task_data.get('result', 'No result provided')
314
+ return f"""A delegated task has completed:
315
+ - Agent: {agent_name}
316
+ - Task ID: {task_id}
317
+ - Status: Completed Successfully
318
+ - Result: {result}
319
+
320
+ IMPORTANT: This task is COMPLETE. Do NOT delegate or call any agent tools.
321
+ Simply report this result to the user in a natural, conversational way."""
322
+
323
+ elif status == 'error':
324
+ error = task_data.get('error', 'Unknown error')
325
+ return f"""A delegated task has failed:
326
+ - Agent: {agent_name}
327
+ - Task ID: {task_id}
328
+ - Status: Error
329
+ - Error: {error}
330
+
331
+ IMPORTANT: This task has FAILED. Do NOT retry or delegate again.
332
+ Report this error to the user and suggest next steps if appropriate."""
333
+
334
+ else:
335
+ return f"""Task update received:
336
+ - Agent: {agent_name}
337
+ - Task ID: {task_id}
338
+ - Status: {status}
339
+ - Data: {task_data}"""
223
340
 
224
341
 
225
342
  async def handle_ai_agent(
@@ -245,11 +362,29 @@ async def handle_ai_agent(
245
362
  """
246
363
  workflow_id = context.get('workflow_id')
247
364
 
248
- # Collect connected memory, skill, tool, and input nodes using shared base function
249
- memory_data, skill_data, tool_data, input_data = await _collect_agent_connections(
365
+ # Collect connected memory, skill, tool, input, and task nodes using shared base function
366
+ memory_data, skill_data, tool_data, input_data, task_data = await _collect_agent_connections(
250
367
  node_id, context, database, log_prefix="[AI Agent]"
251
368
  )
252
369
 
370
+ # If task data is present, format it as context for the agent
371
+ if task_data:
372
+ task_context = _format_task_context(task_data)
373
+ original_prompt = parameters.get('prompt', '')
374
+ parameters = {**parameters, 'prompt': f"{task_context}\n\n{original_prompt}"}
375
+ logger.info(f"[AI Agent] Task context injected for task_id={task_data.get('task_id')}")
376
+
377
+ # CRITICAL FIX: Strip ALL tools when handling task completion
378
+ # When reporting a delegated task result, the agent should NOT use any tools.
379
+ # Binding tools while instructing "do not use tools" confuses Gemini (returns empty []).
380
+ # The agent's only job is to report the result naturally.
381
+ task_status = task_data.get('status', '')
382
+ if task_status in ('completed', 'error') and tool_data:
383
+ original_tool_count = len(tool_data)
384
+ # Strip ALL tools - agent is just reporting result, not executing anything
385
+ tool_data = []
386
+ logger.info(f"[AI Agent] Stripped ALL {original_tool_count} tools for task completion handling")
387
+
253
388
  # Auto-use input data if prompt is empty (fallback for trigger nodes)
254
389
  if not parameters.get('prompt') and input_data:
255
390
  prompt = (
@@ -306,11 +441,29 @@ async def handle_chat_agent(
306
441
  """
307
442
  workflow_id = context.get('workflow_id')
308
443
 
309
- # Collect connected memory, skill, tool, and input nodes using shared base function
310
- memory_data, skill_data, tool_data, input_data = await _collect_agent_connections(
444
+ # Collect connected memory, skill, tool, input, and task nodes using shared base function
445
+ memory_data, skill_data, tool_data, input_data, task_data = await _collect_agent_connections(
311
446
  node_id, context, database, log_prefix="[Chat Agent]"
312
447
  )
313
448
 
449
+ # If task data is present, format it as context for the agent
450
+ if task_data:
451
+ task_context = _format_task_context(task_data)
452
+ original_prompt = parameters.get('prompt', '')
453
+ parameters = {**parameters, 'prompt': f"{task_context}\n\n{original_prompt}"}
454
+ logger.info(f"[Chat Agent] Task context injected for task_id={task_data.get('task_id')}")
455
+
456
+ # CRITICAL FIX: Strip ALL tools when handling task completion
457
+ # When reporting a delegated task result, the agent should NOT use any tools.
458
+ # Binding tools while instructing "do not use tools" confuses Gemini (returns empty []).
459
+ # The agent's only job is to report the result naturally.
460
+ task_status = task_data.get('status', '')
461
+ if task_status in ('completed', 'error') and tool_data:
462
+ original_tool_count = len(tool_data)
463
+ # Strip ALL tools - agent is just reporting result, not executing anything
464
+ tool_data = []
465
+ logger.info(f"[Chat Agent] Stripped ALL {original_tool_count} tools for task completion handling")
466
+
314
467
  # Auto-use input data if prompt is empty (fallback for trigger nodes)
315
468
  if not parameters.get('prompt') and input_data:
316
469
  prompt = (
@@ -370,7 +370,10 @@ async def handle_embedding_generator(
370
370
  logger.info("[embeddingGenerator] Starting", node_id=node_id, texts=len(texts), provider=provider)
371
371
 
372
372
  if provider == 'huggingface':
373
- from langchain_huggingface import HuggingFaceEmbeddings
373
+ try:
374
+ from langchain_huggingface import HuggingFaceEmbeddings
375
+ except ImportError:
376
+ raise ImportError("HuggingFace embeddings not available. Install with: pip install langchain-huggingface sentence-transformers")
374
377
  embedder = HuggingFaceEmbeddings(model_name=model)
375
378
  elif provider == 'openai':
376
379
  from langchain_openai import OpenAIEmbeddings
@@ -448,7 +451,10 @@ async def handle_vector_store(
448
451
 
449
452
  async def _chroma_op(operation: str, params: Dict, collection: str) -> Dict:
450
453
  """ChromaDB operations."""
451
- import chromadb
454
+ try:
455
+ import chromadb
456
+ except ImportError:
457
+ raise ImportError("ChromaDB not available. Install with: pip install chromadb")
452
458
  import uuid
453
459
 
454
460
  persist_dir = params.get('persistDir', './data/vectors')
@@ -500,8 +506,11 @@ async def _chroma_op(operation: str, params: Dict, collection: str) -> Dict:
500
506
 
501
507
  async def _qdrant_op(operation: str, params: Dict, collection: str) -> Dict:
502
508
  """Qdrant operations."""
503
- from qdrant_client import QdrantClient
504
- from qdrant_client.models import VectorParams, Distance, PointStruct
509
+ try:
510
+ from qdrant_client import QdrantClient
511
+ from qdrant_client.models import VectorParams, Distance, PointStruct
512
+ except ImportError:
513
+ raise ImportError("Qdrant client not available. Install with: pip install qdrant-client")
505
514
  import uuid
506
515
 
507
516
  url = params.get('qdrantUrl', 'http://localhost:6333')