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
@@ -0,0 +1,123 @@
1
+ """Periodic cleanup service for long-running daemon.
2
+
3
+ Follows the RecoverySweeper pattern from execution/recovery.py.
4
+ All configuration from Settings (environment variables).
5
+ """
6
+ import asyncio
7
+ import gc
8
+ from typing import Optional, TYPE_CHECKING
9
+
10
+ from core.logging import get_logger
11
+
12
+ if TYPE_CHECKING:
13
+ from core.config import Settings
14
+ from core.database import Database
15
+ from core.cache import CacheService
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ class CleanupService:
21
+ """Background cleanup to prevent resource exhaustion.
22
+
23
+ Periodically cleans up:
24
+ - Expired cache entries
25
+ - Old console logs (keeps configurable count)
26
+ - Old cache entries by age
27
+ - Forces garbage collection
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ database: "Database",
33
+ cache: "CacheService",
34
+ settings: "Settings"
35
+ ):
36
+ self.database = database
37
+ self.cache = cache
38
+ self.settings = settings
39
+ self._running = False
40
+ self._task: Optional[asyncio.Task] = None
41
+
42
+ async def start(self) -> None:
43
+ """Start the cleanup service background task."""
44
+ if self._running:
45
+ return
46
+ self._running = True
47
+ self._task = asyncio.create_task(self._cleanup_loop())
48
+ logger.info(
49
+ "Cleanup service started",
50
+ interval=self.settings.cleanup_interval,
51
+ logs_max=self.settings.cleanup_logs_max_count,
52
+ cache_max_age_hours=self.settings.cleanup_cache_max_age_hours
53
+ )
54
+
55
+ async def stop(self) -> None:
56
+ """Stop the cleanup service gracefully."""
57
+ self._running = False
58
+ if self._task:
59
+ self._task.cancel()
60
+ try:
61
+ await self._task
62
+ except asyncio.CancelledError:
63
+ pass
64
+ logger.info("Cleanup service stopped")
65
+
66
+ async def _cleanup_loop(self) -> None:
67
+ """Main cleanup loop - runs at configured interval."""
68
+ while self._running:
69
+ try:
70
+ await self._run_cleanup()
71
+ except Exception as e:
72
+ logger.error("Cleanup failed", error=str(e))
73
+ await asyncio.sleep(self.settings.cleanup_interval)
74
+
75
+ async def _run_cleanup(self) -> None:
76
+ """Execute all cleanup tasks."""
77
+ results = {}
78
+
79
+ # 1. Expired cache entries (TTL-based)
80
+ try:
81
+ results['expired_cache'] = await self.database.cleanup_expired_cache()
82
+ except Exception as e:
83
+ logger.warning("Failed to cleanup expired cache", error=str(e))
84
+ results['expired_cache'] = 0
85
+
86
+ # 2. Old console logs (keep last N)
87
+ try:
88
+ results['old_logs'] = await self.database.cleanup_old_console_logs(
89
+ keep=self.settings.cleanup_logs_max_count
90
+ )
91
+ except Exception as e:
92
+ logger.warning("Failed to cleanup old console logs", error=str(e))
93
+ results['old_logs'] = 0
94
+
95
+ # 3. Old cache entries by age
96
+ try:
97
+ results['old_cache'] = await self.database.cleanup_old_cache(
98
+ max_age_hours=self.settings.cleanup_cache_max_age_hours
99
+ )
100
+ except Exception as e:
101
+ logger.warning("Failed to cleanup old cache", error=str(e))
102
+ results['old_cache'] = 0
103
+
104
+ # 4. Force garbage collection
105
+ gc.collect()
106
+
107
+ # Only log if something was cleaned up
108
+ total_cleaned = sum(results.values())
109
+ if total_cleaned > 0:
110
+ logger.info("Cleanup completed", **results)
111
+
112
+ async def run_once(self) -> dict:
113
+ """Run cleanup once and return results. Useful for testing."""
114
+ results = {}
115
+ results['expired_cache'] = await self.database.cleanup_expired_cache()
116
+ results['old_logs'] = await self.database.cleanup_old_console_logs(
117
+ keep=self.settings.cleanup_logs_max_count
118
+ )
119
+ results['old_cache'] = await self.database.cleanup_old_cache(
120
+ max_age_hours=self.settings.cleanup_cache_max_age_hours
121
+ )
122
+ gc.collect()
123
+ return results
@@ -100,6 +100,22 @@ class Settings(BaseSettings):
100
100
  # Health Check
101
101
  health_check_interval: int = Field(default=30, env="HEALTH_CHECK_INTERVAL", ge=10)
102
102
 
103
+ # Cleanup Configuration (for long-running daemon)
104
+ cleanup_enabled: bool = Field(default=True, env="CLEANUP_ENABLED")
105
+ cleanup_interval: int = Field(default=3600, env="CLEANUP_INTERVAL", ge=60)
106
+ cleanup_logs_max_count: int = Field(default=1000, env="CLEANUP_LOGS_MAX_COUNT", ge=100)
107
+ cleanup_cache_max_age_hours: int = Field(default=24, env="CLEANUP_CACHE_MAX_AGE_HOURS", ge=1)
108
+
109
+ # Feature Toggles
110
+ ws_logging_enabled: bool = Field(default=True, env="WS_LOGGING_ENABLED")
111
+
112
+ # Gunicorn Configuration (for production deployment)
113
+ gunicorn_timeout: int = Field(default=120, env="GUNICORN_TIMEOUT", ge=30)
114
+ gunicorn_graceful_timeout: int = Field(default=30, env="GUNICORN_GRACEFUL_TIMEOUT", ge=10)
115
+ gunicorn_keepalive: int = Field(default=5, env="GUNICORN_KEEPALIVE", ge=1)
116
+ gunicorn_max_requests: int = Field(default=10000, env="GUNICORN_MAX_REQUESTS", ge=0)
117
+ gunicorn_max_requests_jitter: int = Field(default=1000, env="GUNICORN_MAX_REQUESTS_JITTER", ge=0)
118
+
103
119
  @field_validator("database_url")
104
120
  @classmethod
105
121
  def validate_database_url(cls, v):
@@ -5,7 +5,7 @@ from typing import Dict, Any, List, Optional
5
5
  from sqlmodel import SQLModel, select, Session
6
6
  from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
7
7
  from sqlalchemy.exc import IntegrityError
8
- from sqlalchemy import text
8
+ from sqlalchemy import text, func
9
9
  from contextlib import asynccontextmanager
10
10
 
11
11
  from core.config import Settings
@@ -77,6 +77,12 @@ class Database:
77
77
  "ALTER TABLE user_settings ADD COLUMN console_panel_default_open BOOLEAN DEFAULT 0"
78
78
  ))
79
79
  logger.info("Added console_panel_default_open column to user_settings")
80
+
81
+ if "examples_loaded" not in columns:
82
+ await conn.execute(text(
83
+ "ALTER TABLE user_settings ADD COLUMN examples_loaded BOOLEAN DEFAULT 0"
84
+ ))
85
+ logger.info("Added examples_loaded column to user_settings")
80
86
  except Exception as e:
81
87
  logger.warning(f"Migration check failed (table may not exist yet): {e}")
82
88
 
@@ -489,6 +495,27 @@ class Database:
489
495
  logger.error("Failed to get node output", node_id=node_id, error=str(e))
490
496
  return None
491
497
 
498
+ async def get_node_output_by_session(self, session_id: str,
499
+ output_name: str = "output_0") -> Optional[Dict[str, Any]]:
500
+ """Get node output by session_id only (for delegation result lookup).
501
+
502
+ Used when node_id is unknown but session_id encodes the lookup key.
503
+ """
504
+ try:
505
+ async with self.get_session() as session:
506
+ stmt = select(NodeOutput).where(
507
+ NodeOutput.session_id == session_id,
508
+ NodeOutput.output_name == output_name
509
+ )
510
+ result = await session.execute(stmt)
511
+ output = result.scalar_one_or_none()
512
+
513
+ return {"data": output.data} if output else None
514
+
515
+ except Exception as e:
516
+ logger.error("Failed to get node output by session", session_id=session_id, error=str(e))
517
+ return None
518
+
492
519
  async def delete_node_output(self, node_id: str) -> int:
493
520
  """Delete all outputs for a node (any session). Returns count deleted."""
494
521
  try:
@@ -814,6 +841,47 @@ class Database:
814
841
  logger.error("Failed to clear console logs", error=str(e))
815
842
  return 0
816
843
 
844
+ async def cleanup_old_console_logs(self, keep: int = 1000) -> int:
845
+ """Keep only the most recent N console logs. Returns count deleted."""
846
+ from models.database import ConsoleLog
847
+
848
+ try:
849
+ async with self.get_session() as session:
850
+ # Count total logs
851
+ count_stmt = select(func.count()).select_from(ConsoleLog)
852
+ total_result = await session.execute(count_stmt)
853
+ total = total_result.scalar() or 0
854
+
855
+ if total <= keep:
856
+ return 0
857
+
858
+ # Get IDs of logs to keep (most recent)
859
+ keep_stmt = (
860
+ select(ConsoleLog.id)
861
+ .order_by(ConsoleLog.created_at.desc())
862
+ .limit(keep)
863
+ )
864
+ keep_result = await session.execute(keep_stmt)
865
+ keep_ids = {row[0] for row in keep_result.fetchall()}
866
+
867
+ # Delete logs not in keep list
868
+ delete_stmt = select(ConsoleLog).where(ConsoleLog.id.notin_(keep_ids))
869
+ result = await session.execute(delete_stmt)
870
+ old_logs = result.scalars().all()
871
+
872
+ count = len(old_logs)
873
+ for log in old_logs:
874
+ await session.delete(log)
875
+
876
+ await session.commit()
877
+ if count > 0:
878
+ logger.info("Cleaned up old console logs", deleted=count, kept=keep)
879
+ return count
880
+
881
+ except Exception as e:
882
+ logger.error("Failed to cleanup old console logs", error=str(e))
883
+ return 0
884
+
817
885
  # ============================================================================
818
886
  # Cache Entries (SQLite-backed Redis alternative)
819
887
  # ============================================================================
@@ -941,6 +1009,29 @@ class Database:
941
1009
  logger.error("Failed to cleanup expired cache", error=str(e))
942
1010
  return 0
943
1011
 
1012
+ async def cleanup_old_cache(self, max_age_hours: int = 24) -> int:
1013
+ """Remove cache entries older than max_age_hours. Returns count deleted."""
1014
+ import time
1015
+ try:
1016
+ async with self.get_session() as session:
1017
+ cutoff_time = time.time() - (max_age_hours * 3600)
1018
+ stmt = select(CacheEntry).where(CacheEntry.created_at < cutoff_time)
1019
+ result = await session.execute(stmt)
1020
+ entries = result.scalars().all()
1021
+
1022
+ count = len(entries)
1023
+ for entry in entries:
1024
+ await session.delete(entry)
1025
+
1026
+ await session.commit()
1027
+ if count > 0:
1028
+ logger.info("Cleaned up old cache entries", count=count, max_age_hours=max_age_hours)
1029
+ return count
1030
+
1031
+ except Exception as e:
1032
+ logger.error("Failed to cleanup old cache", error=str(e))
1033
+ return 0
1034
+
944
1035
  async def cache_exists(self, key: str) -> bool:
945
1036
  """Check if cache key exists and is not expired."""
946
1037
  import time
@@ -0,0 +1,121 @@
1
+ """Health check utilities for daemon monitoring.
2
+
3
+ Provides uptime tracking and comprehensive health status for /health endpoint.
4
+ """
5
+ import time
6
+ from typing import Dict, Any, TYPE_CHECKING
7
+
8
+ try:
9
+ import psutil
10
+ PSUTIL_AVAILABLE = True
11
+ except ImportError:
12
+ PSUTIL_AVAILABLE = False
13
+
14
+ if TYPE_CHECKING:
15
+ from core.config import Settings
16
+ from core.database import Database
17
+ from core.cache import CacheService
18
+
19
+ # Module-level startup time tracking
20
+ _startup_time: float = 0.0
21
+
22
+
23
+ def set_startup_time() -> None:
24
+ """Record the application startup time. Call once during lifespan startup."""
25
+ global _startup_time
26
+ _startup_time = time.time()
27
+
28
+
29
+ def get_uptime() -> float:
30
+ """Get uptime in seconds since startup."""
31
+ return time.time() - _startup_time if _startup_time else 0.0
32
+
33
+
34
+ def get_memory_mb() -> float:
35
+ """Get current process memory usage in MB."""
36
+ if not PSUTIL_AVAILABLE:
37
+ return 0.0
38
+ try:
39
+ return psutil.Process().memory_info().rss / (1024 * 1024)
40
+ except Exception:
41
+ return 0.0
42
+
43
+
44
+ def get_disk_percent(path: str = ".") -> float:
45
+ """Get disk usage percentage for given path."""
46
+ if not PSUTIL_AVAILABLE:
47
+ return 0.0
48
+ try:
49
+ return psutil.disk_usage(path).percent
50
+ except Exception:
51
+ return 0.0
52
+
53
+
54
+ def get_cpu_percent() -> float:
55
+ """Get current process CPU usage percentage."""
56
+ if not PSUTIL_AVAILABLE:
57
+ return 0.0
58
+ try:
59
+ return psutil.Process().cpu_percent(interval=0.1)
60
+ except Exception:
61
+ return 0.0
62
+
63
+
64
+ async def check_database(database: "Database") -> bool:
65
+ """Check database connectivity."""
66
+ try:
67
+ async with database.get_session() as session:
68
+ from sqlalchemy import text
69
+ await session.execute(text("SELECT 1"))
70
+ return True
71
+ except Exception:
72
+ return False
73
+
74
+
75
+ async def check_cache(cache: "CacheService") -> bool:
76
+ """Check cache connectivity."""
77
+ try:
78
+ test_key = "_health_check"
79
+ await cache.set(test_key, "ok", ttl=10)
80
+ result = await cache.get(test_key)
81
+ await cache.delete(test_key)
82
+ return result == "ok"
83
+ except Exception:
84
+ return False
85
+
86
+
87
+ async def get_health_status(
88
+ database: "Database",
89
+ cache: "CacheService",
90
+ settings: "Settings"
91
+ ) -> Dict[str, Any]:
92
+ """Get comprehensive health status for /health endpoint.
93
+
94
+ Returns:
95
+ Dict containing status, uptime, resource usage, and feature flags.
96
+ """
97
+ # Run health checks
98
+ db_healthy = await check_database(database)
99
+ cache_healthy = await check_cache(cache)
100
+
101
+ # Determine overall status
102
+ overall_status = "healthy" if (db_healthy and cache_healthy) else "degraded"
103
+
104
+ return {
105
+ "status": overall_status,
106
+ "uptime_seconds": round(get_uptime(), 1),
107
+ "memory_mb": round(get_memory_mb(), 1),
108
+ "disk_percent": round(get_disk_percent(), 1),
109
+ "cpu_percent": round(get_cpu_percent(), 1),
110
+ "checks": {
111
+ "database": db_healthy,
112
+ "cache": cache_healthy,
113
+ },
114
+ "features": {
115
+ "redis": settings.redis_enabled,
116
+ "temporal": settings.temporal_enabled,
117
+ "cleanup": settings.cleanup_enabled,
118
+ "ws_logging": settings.ws_logging_enabled,
119
+ },
120
+ "psutil_available": PSUTIL_AVAILABLE,
121
+ }
@@ -0,0 +1 @@
1
+ # Example workflows package
@@ -0,0 +1,46 @@
1
+ """Gunicorn configuration for production deployment.
2
+
3
+ Reads settings from environment variables (same as config.py).
4
+ No hardcoded values - everything from .env file.
5
+
6
+ Usage:
7
+ gunicorn main:app -c gunicorn.conf.py
8
+ """
9
+ import os
10
+ import multiprocessing
11
+
12
+ # Load from environment (same vars used by config.py)
13
+ host = os.getenv("HOST", "0.0.0.0")
14
+ port = os.getenv("PORT", "3010")
15
+ workers_env = os.getenv("WORKERS", "0") # 0 = auto-calculate
16
+ log_level = os.getenv("LOG_LEVEL", "INFO").lower()
17
+ debug = os.getenv("DEBUG", "false").lower() == "true"
18
+
19
+ # Bind - use HOST and PORT from .env
20
+ bind = f"{host}:{port}"
21
+
22
+ # Workers - from WORKERS env or auto-calculate
23
+ # WORKERS=0 means auto (2 * cpu + 1), WORKERS=N means use N
24
+ workers_count = int(workers_env)
25
+ workers = workers_count if workers_count > 0 else (multiprocessing.cpu_count() * 2 + 1)
26
+ worker_class = "uvicorn.workers.UvicornWorker"
27
+
28
+ # Timeouts - configurable via env
29
+ timeout = int(os.getenv("GUNICORN_TIMEOUT", "120"))
30
+ graceful_timeout = int(os.getenv("GUNICORN_GRACEFUL_TIMEOUT", "30"))
31
+ keepalive = int(os.getenv("GUNICORN_KEEPALIVE", "5"))
32
+
33
+ # Restart workers periodically to prevent memory leaks
34
+ max_requests = int(os.getenv("GUNICORN_MAX_REQUESTS", "10000"))
35
+ max_requests_jitter = int(os.getenv("GUNICORN_MAX_REQUESTS_JITTER", "1000"))
36
+
37
+ # Logging - use LOG_LEVEL from .env
38
+ accesslog = "-" if not debug else None
39
+ errorlog = "-"
40
+ loglevel = log_level
41
+
42
+ # Process naming
43
+ proc_name = "machina-backend"
44
+
45
+ # Preload app for faster worker startup (disable in debug for reload)
46
+ preload_app = not debug
package/server/main.py CHANGED
@@ -104,6 +104,21 @@ async def lifespan(app: FastAPI):
104
104
  setup_websocket_logging(loop)
105
105
  logger.info("WebSocket logging handler started")
106
106
 
107
+ # Start cleanup service for long-running daemon
108
+ from core.cleanup import CleanupService
109
+ from core.health import set_startup_time
110
+ cleanup_service = None
111
+ if settings.cleanup_enabled:
112
+ cleanup_service = CleanupService(
113
+ database=container.database(),
114
+ cache=container.cache(),
115
+ settings=settings
116
+ )
117
+ await cleanup_service.start()
118
+
119
+ # Record startup time for health reporting
120
+ set_startup_time()
121
+
107
122
  # Initialize Temporal if enabled
108
123
  temporal_worker_manager = None
109
124
  if settings.temporal_enabled:
@@ -165,6 +180,10 @@ async def lifespan(app: FastAPI):
165
180
  from services.android.manager import close_relay_client
166
181
  await close_relay_client(clear_stored_session=False)
167
182
 
183
+ # Stop cleanup service
184
+ if cleanup_service is not None:
185
+ await cleanup_service.stop()
186
+
168
187
  # Stop recovery sweeper first
169
188
  if settings.redis_enabled:
170
189
  await recovery_sweeper.stop()
@@ -239,12 +258,20 @@ app.include_router(webhook.router)
239
258
 
240
259
  @app.get("/health")
241
260
  async def health_check():
242
- """Detailed health check."""
261
+ """Detailed health check with resource monitoring."""
243
262
  from services import event_waiter
244
263
  from services.execution import get_recovery_sweeper
264
+ from core.health import get_health_status, get_uptime, get_memory_mb, get_disk_percent, get_cpu_percent
245
265
 
246
266
  sweeper = get_recovery_sweeper()
247
267
 
268
+ # Get comprehensive health status
269
+ health = await get_health_status(
270
+ database=container.database(),
271
+ cache=container.cache(),
272
+ settings=settings
273
+ )
274
+
248
275
  # Check Temporal status
249
276
  temporal_status = {
250
277
  "enabled": settings.temporal_enabled,
@@ -259,10 +286,18 @@ async def health_check():
259
286
  pass
260
287
 
261
288
  return {
262
- "status": "OK",
289
+ "status": health["status"],
263
290
  "service": "python",
264
- "version": "3.2.0", # Bumped for Temporal integration
291
+ "version": "3.3.0", # Bumped for daemon service support
265
292
  "environment": "development" if settings.debug else "production",
293
+ "uptime_seconds": health["uptime_seconds"],
294
+ "resources": {
295
+ "memory_mb": health["memory_mb"],
296
+ "disk_percent": health["disk_percent"],
297
+ "cpu_percent": health["cpu_percent"],
298
+ },
299
+ "checks": health["checks"],
300
+ "features": health["features"],
266
301
  "redis_enabled": settings.redis_enabled,
267
302
  "event_waiter_mode": event_waiter.get_backend_mode(),
268
303
  "execution_engine": {
@@ -252,6 +252,7 @@ class UserSettings(SQLModel, table=True):
252
252
  sidebar_default_open: bool = Field(default=True)
253
253
  component_palette_default_open: bool = Field(default=True)
254
254
  console_panel_default_open: bool = Field(default=False)
255
+ examples_loaded: bool = Field(default=False) # Track if example workflows were imported
255
256
  created_at: datetime = Field(
256
257
  default_factory=lambda: datetime.now(timezone.utc),
257
258
  sa_column=Column(DateTime(timezone=True), server_default=func.now())
@@ -147,6 +147,22 @@ class WhatsAppReceiveParams(BaseNodeParams):
147
147
  include_media: bool = Field(default=False, alias="includeMediaData")
148
148
 
149
149
 
150
+ class WhatsAppDbParams(BaseNodeParams):
151
+ """Parameters for WhatsApp database query node."""
152
+ type: Literal["whatsappDb"]
153
+ operation: Literal["chat_history", "search_groups", "get_group_info", "get_contact_info", "list_contacts", "check_contacts"] = "chat_history"
154
+ # chat_history params
155
+ phone_number: str = Field(default="", alias="phoneNumber")
156
+ group_id: str = Field(default="", alias="groupId")
157
+ group_name: str = Field(default="", alias="groupName")
158
+ limit: int = Field(default=50, ge=1, le=500)
159
+ offset: int = Field(default=0, ge=0)
160
+ # search_groups params
161
+ search_query: str = Field(default="", alias="searchQuery")
162
+ # check_contacts params
163
+ phone_numbers: str = Field(default="", alias="phoneNumbers")
164
+
165
+
150
166
  # =============================================================================
151
167
  # CODE EXECUTOR NODE MODELS
152
168
  # =============================================================================
@@ -277,7 +293,7 @@ MapsNodeParams = Annotated[
277
293
 
278
294
  # WhatsApp Nodes
279
295
  WhatsAppNodeParams = Annotated[
280
- Union[WhatsAppSendParams, WhatsAppReceiveParams],
296
+ Union[WhatsAppSendParams, WhatsAppReceiveParams, WhatsAppDbParams],
281
297
  Field(discriminator="type")
282
298
  ]
283
299
 
@@ -319,7 +335,7 @@ KnownNodeParams = Annotated[
319
335
  # Maps
320
336
  CreateMapParams, GmapsLocationsParams, GmapsNearbyPlacesParams,
321
337
  # WhatsApp
322
- WhatsAppSendParams, WhatsAppReceiveParams,
338
+ WhatsAppSendParams, WhatsAppReceiveParams, WhatsAppDbParams,
323
339
  # Code
324
340
  PythonExecutorParams, JavaScriptExecutorParams,
325
341
  # HTTP
@@ -0,0 +1,86 @@
1
+ # Docker-specific requirements (CPU-only, no CUDA)
2
+ # This file is used by Dockerfile to avoid large CUDA dependencies
3
+
4
+ # Core Framework
5
+ fastapi>=0.110.0
6
+ uvicorn[standard]>=0.27.0
7
+
8
+ # Performance optimization
9
+ uvloop>=0.19.0;sys_platform!='win32'
10
+ orjson>=3.10.0
11
+
12
+ # Database & ORM (Python 3.13 compatible versions)
13
+ sqlmodel>=0.0.18
14
+ sqlalchemy[asyncio]>=2.0.35
15
+ aiosqlite>=0.20.0
16
+
17
+ # Settings & Configuration
18
+ pydantic>=2.6.0
19
+ pydantic-settings>=2.2.0
20
+ python-dotenv>=1.0.0
21
+
22
+ # Dependency Injection
23
+ dependency-injector>=4.41.0
24
+
25
+ # Caching (Redis) - Python 3.13 compatible
26
+ redis>=5.0.0
27
+
28
+ # HTTP Client
29
+ httpx>=0.27.0
30
+
31
+ # WebSocket Client (for WhatsApp RPC)
32
+ websockets>=12.0
33
+
34
+ # Async HTTP Client (for Android relay WebSocket)
35
+ aiohttp>=3.9.0
36
+
37
+ # QR Code Generation (for WhatsApp QR display)
38
+ qrcode[pil]>=7.4.0
39
+
40
+ # Logging
41
+ structlog>=23.2.0
42
+
43
+ # AI Providers (LangChain) - compatible versions
44
+ langchain-core>=1.2.6
45
+ langchain-openai>=1.0.0
46
+ langchain-anthropic>=1.3.0
47
+ langchain-google-genai>=3.1.0
48
+ langchain-groq>=0.1.0
49
+ langchain-cerebras>=0.1.0
50
+
51
+ # LangGraph - State machine for AI agents
52
+ langgraph>=1.0.6
53
+
54
+ # Temporal - Durable workflow execution (optional)
55
+ temporalio>=1.21.1
56
+
57
+ # Google Services
58
+ googlemaps>=4.10.0
59
+
60
+ # Scheduling
61
+ APScheduler>=3.10.0
62
+
63
+ # Web Search
64
+ ddgs>=9.0.0
65
+
66
+ # Authentication
67
+ bcrypt>=4.1.0
68
+ python-jose[cryptography]>=3.3.0
69
+ email-validator>=2.0.0
70
+
71
+ # Timezone support
72
+ pytz>=2024.1
73
+
74
+ # Document Processing (CPU-only)
75
+ beautifulsoup4>=4.12.0
76
+ langchain-text-splitters>=0.3.0
77
+ pypdf>=4.0.0
78
+
79
+ # Note: The following are excluded for Docker builds (CUDA/GPU heavy):
80
+ # - sentence-transformers (pulls PyTorch with CUDA)
81
+ # - langchain-huggingface (pulls sentence-transformers)
82
+ # - chromadb (pulls sentence-transformers for embeddings)
83
+ # - qdrant-client (optional, can be re-added if needed without embeddings)
84
+ #
85
+ # For production with embeddings, use external embedding APIs (OpenAI, etc.)
86
+ # or build a separate GPU-enabled image.
@@ -7,6 +7,7 @@ from typing import Dict, Any, Optional, List
7
7
  from core.container import container
8
8
  from core.database import Database
9
9
  from core.logging import get_logger
10
+ from services.example_loader import import_examples_for_user
10
11
 
11
12
  logger = get_logger(__name__)
12
13
  router = APIRouter(prefix="/api/database", tags=["database"])
@@ -93,6 +94,21 @@ async def get_all_workflows(
93
94
  ):
94
95
  """Get all workflows."""
95
96
  try:
97
+ # Auto-load example workflows on first fetch
98
+ user_id = "default"
99
+ settings = await database.get_user_settings(user_id)
100
+
101
+ if not settings or not settings.get("examples_loaded", False):
102
+ # First time - import examples
103
+ count = await import_examples_for_user(database)
104
+ if count > 0:
105
+ logger.info(f"Auto-loaded {count} example workflows")
106
+
107
+ # Mark as loaded using existing save_user_settings
108
+ current = settings or {}
109
+ current["examples_loaded"] = True
110
+ await database.save_user_settings(current, user_id)
111
+
96
112
  workflows = await database.get_all_workflows()
97
113
  return {
98
114
  "success": True,