machinaos 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/.env.template +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/bin/cli.js +159 -0
  5. package/client/.dockerignore +45 -0
  6. package/client/Dockerfile +68 -0
  7. package/client/eslint.config.js +29 -0
  8. package/client/index.html +13 -0
  9. package/client/nginx.conf +66 -0
  10. package/client/package.json +48 -0
  11. package/client/src/App.tsx +27 -0
  12. package/client/src/Dashboard.tsx +1173 -0
  13. package/client/src/ParameterPanel.tsx +301 -0
  14. package/client/src/components/AIAgentNode.tsx +321 -0
  15. package/client/src/components/APIKeyValidator.tsx +118 -0
  16. package/client/src/components/ClaudeChatModelNode.tsx +18 -0
  17. package/client/src/components/ConditionalEdge.tsx +189 -0
  18. package/client/src/components/CredentialsModal.tsx +306 -0
  19. package/client/src/components/EdgeConditionEditor.tsx +443 -0
  20. package/client/src/components/GeminiChatModelNode.tsx +18 -0
  21. package/client/src/components/GenericNode.tsx +357 -0
  22. package/client/src/components/LocationParameterPanel.tsx +154 -0
  23. package/client/src/components/ModelNode.tsx +286 -0
  24. package/client/src/components/OpenAIChatModelNode.tsx +18 -0
  25. package/client/src/components/OutputPanel.tsx +471 -0
  26. package/client/src/components/ParameterRenderer.tsx +1874 -0
  27. package/client/src/components/SkillEditorModal.tsx +417 -0
  28. package/client/src/components/SquareNode.tsx +797 -0
  29. package/client/src/components/StartNode.tsx +250 -0
  30. package/client/src/components/ToolkitNode.tsx +365 -0
  31. package/client/src/components/TriggerNode.tsx +463 -0
  32. package/client/src/components/auth/LoginPage.tsx +247 -0
  33. package/client/src/components/auth/ProtectedRoute.tsx +59 -0
  34. package/client/src/components/base/BaseChatModelNode.tsx +271 -0
  35. package/client/src/components/icons/AIProviderIcons.tsx +50 -0
  36. package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
  37. package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
  38. package/client/src/components/maps/index.ts +26 -0
  39. package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
  40. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
  41. package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
  42. package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
  43. package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
  44. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
  45. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
  46. package/client/src/components/parameterPanel/index.ts +42 -0
  47. package/client/src/components/shared/DataPanel.tsx +142 -0
  48. package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
  49. package/client/src/components/ui/AIResultModal.tsx +204 -0
  50. package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
  51. package/client/src/components/ui/CodeEditor.tsx +81 -0
  52. package/client/src/components/ui/CollapsibleSection.tsx +88 -0
  53. package/client/src/components/ui/ComponentItem.tsx +154 -0
  54. package/client/src/components/ui/ComponentPalette.tsx +321 -0
  55. package/client/src/components/ui/ConsolePanel.tsx +1074 -0
  56. package/client/src/components/ui/ErrorBoundary.tsx +196 -0
  57. package/client/src/components/ui/InputNodesPanel.tsx +204 -0
  58. package/client/src/components/ui/MapSelector.tsx +314 -0
  59. package/client/src/components/ui/Modal.tsx +149 -0
  60. package/client/src/components/ui/NodeContextMenu.tsx +192 -0
  61. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
  62. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
  63. package/client/src/components/ui/SettingsPanel.tsx +243 -0
  64. package/client/src/components/ui/TopToolbar.tsx +736 -0
  65. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
  66. package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
  67. package/client/src/config/antdTheme.ts +186 -0
  68. package/client/src/config/api.ts +54 -0
  69. package/client/src/contexts/AuthContext.tsx +221 -0
  70. package/client/src/contexts/ThemeContext.tsx +42 -0
  71. package/client/src/contexts/WebSocketContext.tsx +1971 -0
  72. package/client/src/factories/baseChatModelFactory.ts +256 -0
  73. package/client/src/hooks/useAndroidOperations.ts +164 -0
  74. package/client/src/hooks/useApiKeyValidation.ts +107 -0
  75. package/client/src/hooks/useApiKeys.ts +238 -0
  76. package/client/src/hooks/useAppTheme.ts +17 -0
  77. package/client/src/hooks/useComponentPalette.ts +51 -0
  78. package/client/src/hooks/useCopyPaste.ts +155 -0
  79. package/client/src/hooks/useDragAndDrop.ts +124 -0
  80. package/client/src/hooks/useDragVariable.ts +88 -0
  81. package/client/src/hooks/useExecution.ts +313 -0
  82. package/client/src/hooks/useParameterPanel.ts +176 -0
  83. package/client/src/hooks/useReactFlowNodes.ts +189 -0
  84. package/client/src/hooks/useToolSchema.ts +209 -0
  85. package/client/src/hooks/useWhatsApp.ts +196 -0
  86. package/client/src/hooks/useWorkflowManagement.ts +46 -0
  87. package/client/src/index.css +315 -0
  88. package/client/src/main.tsx +19 -0
  89. package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
  90. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
  91. package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
  92. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
  93. package/client/src/nodeDefinitions/chatNodes.ts +135 -0
  94. package/client/src/nodeDefinitions/codeNodes.ts +54 -0
  95. package/client/src/nodeDefinitions/documentNodes.ts +379 -0
  96. package/client/src/nodeDefinitions/index.ts +15 -0
  97. package/client/src/nodeDefinitions/locationNodes.ts +463 -0
  98. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
  99. package/client/src/nodeDefinitions/skillNodes.ts +211 -0
  100. package/client/src/nodeDefinitions/toolNodes.ts +198 -0
  101. package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
  102. package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
  103. package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
  104. package/client/src/nodeDefinitions.ts +104 -0
  105. package/client/src/schemas/workflowSchema.ts +264 -0
  106. package/client/src/services/dynamicParameterService.ts +96 -0
  107. package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
  108. package/client/src/services/executionService.ts +232 -0
  109. package/client/src/services/workflowApi.ts +91 -0
  110. package/client/src/store/useAppStore.ts +582 -0
  111. package/client/src/styles/theme.ts +508 -0
  112. package/client/src/styles/zIndex.ts +17 -0
  113. package/client/src/types/ComponentTypes.ts +39 -0
  114. package/client/src/types/EdgeCondition.ts +231 -0
  115. package/client/src/types/INodeProperties.ts +288 -0
  116. package/client/src/types/NodeTypes.ts +28 -0
  117. package/client/src/utils/formatters.ts +33 -0
  118. package/client/src/utils/googleMapsLoader.ts +140 -0
  119. package/client/src/utils/locationUtils.ts +85 -0
  120. package/client/src/utils/nodeUtils.ts +31 -0
  121. package/client/src/utils/workflow.ts +30 -0
  122. package/client/src/utils/workflowExport.ts +120 -0
  123. package/client/src/vite-env.d.ts +12 -0
  124. package/client/tailwind.config.js +60 -0
  125. package/client/tsconfig.json +25 -0
  126. package/client/tsconfig.node.json +11 -0
  127. package/client/vite.config.js +35 -0
  128. package/docker-compose.prod.yml +107 -0
  129. package/docker-compose.yml +104 -0
  130. package/docs-MachinaOs/README.md +85 -0
  131. package/docs-MachinaOs/deployment/docker.mdx +228 -0
  132. package/docs-MachinaOs/deployment/production.mdx +345 -0
  133. package/docs-MachinaOs/docs.json +75 -0
  134. package/docs-MachinaOs/faq.mdx +309 -0
  135. package/docs-MachinaOs/favicon.svg +5 -0
  136. package/docs-MachinaOs/installation.mdx +160 -0
  137. package/docs-MachinaOs/introduction.mdx +114 -0
  138. package/docs-MachinaOs/logo/dark.svg +6 -0
  139. package/docs-MachinaOs/logo/light.svg +6 -0
  140. package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
  141. package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
  142. package/docs-MachinaOs/nodes/android.mdx +411 -0
  143. package/docs-MachinaOs/nodes/overview.mdx +181 -0
  144. package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
  145. package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
  146. package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
  147. package/docs-MachinaOs/quickstart.mdx +119 -0
  148. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
  149. package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
  150. package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
  151. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
  152. package/nul +0 -0
  153. package/package.json +70 -0
  154. package/scripts/build.js +158 -0
  155. package/scripts/check-ports.ps1 +33 -0
  156. package/scripts/clean.js +40 -0
  157. package/scripts/docker.js +93 -0
  158. package/scripts/kill-port.ps1 +154 -0
  159. package/scripts/start.js +210 -0
  160. package/scripts/stop.js +325 -0
  161. package/server/.dockerignore +44 -0
  162. package/server/Dockerfile +45 -0
  163. package/server/constants.py +249 -0
  164. package/server/core/__init__.py +1 -0
  165. package/server/core/cache.py +461 -0
  166. package/server/core/config.py +128 -0
  167. package/server/core/container.py +99 -0
  168. package/server/core/database.py +1211 -0
  169. package/server/core/logging.py +314 -0
  170. package/server/main.py +289 -0
  171. package/server/middleware/__init__.py +5 -0
  172. package/server/middleware/auth.py +89 -0
  173. package/server/models/__init__.py +1 -0
  174. package/server/models/auth.py +52 -0
  175. package/server/models/cache.py +24 -0
  176. package/server/models/database.py +211 -0
  177. package/server/models/nodes.py +455 -0
  178. package/server/package.json +9 -0
  179. package/server/pyproject.toml +72 -0
  180. package/server/requirements.txt +83 -0
  181. package/server/routers/__init__.py +1 -0
  182. package/server/routers/android.py +294 -0
  183. package/server/routers/auth.py +203 -0
  184. package/server/routers/database.py +151 -0
  185. package/server/routers/maps.py +142 -0
  186. package/server/routers/nodejs_compat.py +289 -0
  187. package/server/routers/webhook.py +90 -0
  188. package/server/routers/websocket.py +2127 -0
  189. package/server/routers/whatsapp.py +761 -0
  190. package/server/routers/workflow.py +200 -0
  191. package/server/services/__init__.py +1 -0
  192. package/server/services/ai.py +2415 -0
  193. package/server/services/android/__init__.py +27 -0
  194. package/server/services/android/broadcaster.py +114 -0
  195. package/server/services/android/client.py +608 -0
  196. package/server/services/android/manager.py +78 -0
  197. package/server/services/android/protocol.py +165 -0
  198. package/server/services/android_service.py +588 -0
  199. package/server/services/auth.py +131 -0
  200. package/server/services/chat_client.py +160 -0
  201. package/server/services/deployment/__init__.py +12 -0
  202. package/server/services/deployment/manager.py +706 -0
  203. package/server/services/deployment/state.py +47 -0
  204. package/server/services/deployment/triggers.py +275 -0
  205. package/server/services/event_waiter.py +785 -0
  206. package/server/services/execution/__init__.py +77 -0
  207. package/server/services/execution/cache.py +769 -0
  208. package/server/services/execution/conditions.py +373 -0
  209. package/server/services/execution/dlq.py +132 -0
  210. package/server/services/execution/executor.py +1351 -0
  211. package/server/services/execution/models.py +531 -0
  212. package/server/services/execution/recovery.py +235 -0
  213. package/server/services/handlers/__init__.py +126 -0
  214. package/server/services/handlers/ai.py +355 -0
  215. package/server/services/handlers/android.py +260 -0
  216. package/server/services/handlers/code.py +278 -0
  217. package/server/services/handlers/document.py +598 -0
  218. package/server/services/handlers/http.py +193 -0
  219. package/server/services/handlers/polyglot.py +105 -0
  220. package/server/services/handlers/tools.py +845 -0
  221. package/server/services/handlers/triggers.py +107 -0
  222. package/server/services/handlers/utility.py +822 -0
  223. package/server/services/handlers/whatsapp.py +476 -0
  224. package/server/services/maps.py +289 -0
  225. package/server/services/memory_store.py +103 -0
  226. package/server/services/node_executor.py +375 -0
  227. package/server/services/parameter_resolver.py +218 -0
  228. package/server/services/polyglot_client.py +169 -0
  229. package/server/services/scheduler.py +155 -0
  230. package/server/services/skill_loader.py +417 -0
  231. package/server/services/status_broadcaster.py +826 -0
  232. package/server/services/temporal/__init__.py +23 -0
  233. package/server/services/temporal/activities.py +344 -0
  234. package/server/services/temporal/client.py +76 -0
  235. package/server/services/temporal/executor.py +147 -0
  236. package/server/services/temporal/worker.py +251 -0
  237. package/server/services/temporal/workflow.py +355 -0
  238. package/server/services/temporal/ws_client.py +236 -0
  239. package/server/services/text.py +111 -0
  240. package/server/services/user_auth.py +172 -0
  241. package/server/services/websocket_client.py +29 -0
  242. package/server/services/workflow.py +597 -0
  243. package/server/skills/android-skill/SKILL.md +82 -0
  244. package/server/skills/assistant-personality/SKILL.md +45 -0
  245. package/server/skills/code-skill/SKILL.md +140 -0
  246. package/server/skills/http-skill/SKILL.md +161 -0
  247. package/server/skills/maps-skill/SKILL.md +170 -0
  248. package/server/skills/memory-skill/SKILL.md +154 -0
  249. package/server/skills/scheduler-skill/SKILL.md +84 -0
  250. package/server/skills/whatsapp-skill/SKILL.md +283 -0
  251. package/server/uv.lock +2916 -0
  252. package/server/whatsapp-rpc/.dockerignore +30 -0
  253. package/server/whatsapp-rpc/Dockerfile +44 -0
  254. package/server/whatsapp-rpc/Dockerfile.web +17 -0
  255. package/server/whatsapp-rpc/README.md +139 -0
  256. package/server/whatsapp-rpc/cli.js +95 -0
  257. package/server/whatsapp-rpc/configs/config.yaml +7 -0
  258. package/server/whatsapp-rpc/docker-compose.yml +35 -0
  259. package/server/whatsapp-rpc/docs/API.md +410 -0
  260. package/server/whatsapp-rpc/go.mod +67 -0
  261. package/server/whatsapp-rpc/go.sum +203 -0
  262. package/server/whatsapp-rpc/package.json +30 -0
  263. package/server/whatsapp-rpc/schema.json +1294 -0
  264. package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
  265. package/server/whatsapp-rpc/scripts/cli.js +162 -0
  266. package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
  267. package/server/whatsapp-rpc/src/go/config/config.go +49 -0
  268. package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
  269. package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
  270. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
  271. package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
  272. package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
  273. package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
  274. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
  275. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
  276. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
  277. package/server/whatsapp-rpc/web/app.py +609 -0
  278. package/server/whatsapp-rpc/web/requirements.txt +6 -0
  279. package/server/whatsapp-rpc/web/rpc_client.py +427 -0
  280. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
  281. package/server/whatsapp-rpc/web/templates/base.html +150 -0
  282. package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
  283. package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
  284. package/server/whatsapp-rpc/web/templates/groups.html +328 -0
  285. package/server/whatsapp-rpc/web/templates/messages.html +465 -0
  286. package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
  287. package/server/whatsapp-rpc/web/templates/send.html +259 -0
  288. package/server/whatsapp-rpc/web/templates/settings.html +459 -0
@@ -0,0 +1,461 @@
1
+ """Cache service with Redis (production) or SQLite (development) backend.
2
+
3
+ Follows n8n pattern where SQLite is sufficient for single-process deployments,
4
+ with Redis used only for distributed queue mode or high-performance needs.
5
+ """
6
+
7
+ import json
8
+ import asyncio
9
+ from typing import Any, Dict, Optional, List, TYPE_CHECKING
10
+ from datetime import timedelta
11
+
12
+ try:
13
+ import redis.asyncio as redis
14
+ REDIS_AVAILABLE = True
15
+ except ImportError:
16
+ redis = None
17
+ REDIS_AVAILABLE = False
18
+
19
+ from core.config import Settings
20
+ from core.logging import get_logger, log_cache_operation
21
+
22
+ if TYPE_CHECKING:
23
+ from core.database import Database
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class CacheService:
29
+ """Async cache service with Redis or SQLite backend.
30
+
31
+ Backend selection:
32
+ - Redis: When REDIS_ENABLED=true and Redis is available (production)
33
+ - SQLite: When Redis disabled or unavailable (development)
34
+ - Memory: Temporary fallback if both fail
35
+ """
36
+
37
+ def __init__(self, settings: Settings, database: Optional["Database"] = None):
38
+ self.settings = settings
39
+ self.database = database # SQLite backend
40
+ self.redis: Optional[redis.Redis] = None
41
+ self.memory_cache: Dict[str, Any] = {} # Emergency fallback only
42
+ self.use_redis = settings.redis_enabled and REDIS_AVAILABLE
43
+ self.use_sqlite = not self.use_redis and database is not None
44
+ self._streams_available = False # Checked during startup
45
+
46
+ async def startup(self):
47
+ """Initialize cache connection."""
48
+ if self.use_redis and self.settings.redis_url:
49
+ try:
50
+ self.redis = redis.from_url(
51
+ self.settings.redis_url,
52
+ encoding="utf-8",
53
+ decode_responses=True,
54
+ socket_timeout=5,
55
+ socket_connect_timeout=5,
56
+ retry_on_timeout=True
57
+ )
58
+
59
+ # Test connection
60
+ await self.redis.ping()
61
+ logger.info("Redis cache initialized", url=self.settings.redis_url)
62
+
63
+ # Test Redis Streams availability (required for trigger nodes)
64
+ await self._check_streams_support()
65
+
66
+ except Exception as e:
67
+ logger.warning("Redis connection failed, falling back", error=str(e))
68
+ self.use_redis = False
69
+ self.redis = None
70
+ # Try SQLite fallback
71
+ if self.database:
72
+ self.use_sqlite = True
73
+ logger.info("Using SQLite cache (Redis fallback)")
74
+ else:
75
+ if self.use_sqlite:
76
+ logger.info("Using SQLite cache (n8n pattern - no Redis required for single-process)")
77
+ else:
78
+ logger.info("Using in-memory cache",
79
+ redis_enabled=self.settings.redis_enabled,
80
+ redis_available=REDIS_AVAILABLE)
81
+
82
+ async def _check_streams_support(self):
83
+ """Check if Redis supports Streams (XADD/XREAD commands).
84
+
85
+ Some Redis-compatible services (e.g., certain cloud providers) don't support Streams.
86
+ We test this at startup to avoid runtime failures in trigger nodes.
87
+ """
88
+ if not self.redis:
89
+ self._streams_available = False
90
+ return
91
+
92
+ test_stream = "_machina_streams_test"
93
+ try:
94
+ # Try XADD - this will fail if Streams aren't supported
95
+ msg_id = await self.redis.xadd(test_stream, {"test": "1"}, maxlen=1)
96
+ if msg_id:
97
+ # Clean up test stream
98
+ await self.redis.delete(test_stream)
99
+ self._streams_available = True
100
+ logger.info("Redis Streams available - trigger nodes will use Redis persistence")
101
+ else:
102
+ self._streams_available = False
103
+ logger.warning("Redis Streams test failed - trigger nodes will use memory mode")
104
+ except Exception as e:
105
+ self._streams_available = False
106
+ error_str = str(e).lower()
107
+ if "unknown command" in error_str:
108
+ logger.warning("Redis Streams not supported by server - trigger nodes will use memory mode")
109
+ else:
110
+ logger.warning(f"Redis Streams check failed: {e} - trigger nodes will use memory mode")
111
+
112
+ async def shutdown(self):
113
+ """Close cache connections."""
114
+ if self.redis:
115
+ await self.redis.close()
116
+ logger.info("Redis cache connections closed")
117
+
118
+ # Clear memory cache
119
+ self.memory_cache.clear()
120
+
121
+ async def get(self, key: str) -> Optional[Any]:
122
+ """Get value from cache."""
123
+ try:
124
+ if self.use_redis and self.redis:
125
+ value = await self.redis.get(key)
126
+ if value:
127
+ log_cache_operation(logger, "get", key, hit=True)
128
+ return json.loads(value)
129
+ else:
130
+ log_cache_operation(logger, "get", key, hit=False)
131
+ return None
132
+ elif self.use_sqlite and self.database:
133
+ # SQLite cache
134
+ value = await self.database.get_cache_entry(key)
135
+ if value:
136
+ log_cache_operation(logger, "get", key, hit=True)
137
+ return json.loads(value)
138
+ else:
139
+ log_cache_operation(logger, "get", key, hit=False)
140
+ return None
141
+ else:
142
+ # Memory cache fallback
143
+ value = self.memory_cache.get(key)
144
+ log_cache_operation(logger, "get", key, hit=value is not None)
145
+ return value
146
+
147
+ except Exception as e:
148
+ logger.error("Cache get failed", key=key, error=str(e))
149
+ return None
150
+
151
+ async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
152
+ """Set value in cache with optional TTL."""
153
+ try:
154
+ ttl = ttl or self.settings.cache_ttl
155
+
156
+ if self.use_redis and self.redis:
157
+ serialized = json.dumps(value, default=str)
158
+ await self.redis.setex(key, ttl, serialized)
159
+ log_cache_operation(logger, "set", key, ttl=ttl)
160
+ return True
161
+ elif self.use_sqlite and self.database:
162
+ # SQLite cache with TTL support
163
+ serialized = json.dumps(value, default=str)
164
+ await self.database.set_cache_entry(key, serialized, ttl)
165
+ log_cache_operation(logger, "set", key, ttl=ttl)
166
+ return True
167
+ else:
168
+ # Memory cache fallback (no TTL)
169
+ self.memory_cache[key] = value
170
+ log_cache_operation(logger, "set", key, ttl=ttl)
171
+ return True
172
+
173
+ except Exception as e:
174
+ logger.error("Cache set failed", key=key, error=str(e))
175
+ return False
176
+
177
+ async def delete(self, key: str) -> bool:
178
+ """Delete value from cache."""
179
+ try:
180
+ if self.use_redis and self.redis:
181
+ deleted = await self.redis.delete(key)
182
+ log_cache_operation(logger, "delete", key, deleted=bool(deleted))
183
+ return bool(deleted)
184
+ elif self.use_sqlite and self.database:
185
+ # SQLite cache
186
+ deleted = await self.database.delete_cache_entry(key)
187
+ log_cache_operation(logger, "delete", key, deleted=deleted)
188
+ return deleted
189
+ else:
190
+ # Memory cache fallback
191
+ deleted = key in self.memory_cache
192
+ if deleted:
193
+ del self.memory_cache[key]
194
+ log_cache_operation(logger, "delete", key, deleted=deleted)
195
+ return deleted
196
+
197
+ except Exception as e:
198
+ logger.error("Cache delete failed", key=key, error=str(e))
199
+ return False
200
+
201
+ async def exists(self, key: str) -> bool:
202
+ """Check if key exists in cache."""
203
+ try:
204
+ if self.use_redis and self.redis:
205
+ exists = await self.redis.exists(key)
206
+ return bool(exists)
207
+ elif self.use_sqlite and self.database:
208
+ return await self.database.cache_exists(key)
209
+ else:
210
+ return key in self.memory_cache
211
+
212
+ except Exception as e:
213
+ logger.error("Cache exists check failed", key=key, error=str(e))
214
+ return False
215
+
216
+ async def expire(self, key: str, ttl: int) -> bool:
217
+ """Set TTL for existing key."""
218
+ try:
219
+ if self.use_redis and self.redis:
220
+ return bool(await self.redis.expire(key, ttl))
221
+ else:
222
+ # Memory cache doesn't support TTL updates
223
+ return key in self.memory_cache
224
+
225
+ except Exception as e:
226
+ logger.error("Cache expire failed", key=key, error=str(e))
227
+ return False
228
+
229
+ async def clear_pattern(self, pattern: str) -> int:
230
+ """Clear keys matching pattern."""
231
+ try:
232
+ if self.use_redis and self.redis:
233
+ keys = await self.redis.keys(pattern)
234
+ if keys:
235
+ deleted = await self.redis.delete(*keys)
236
+ log_cache_operation(logger, "clear_pattern", pattern, deleted=deleted)
237
+ return deleted
238
+ return 0
239
+ elif self.use_sqlite and self.database:
240
+ # SQLite cache pattern matching
241
+ deleted = await self.database.delete_cache_pattern(pattern)
242
+ log_cache_operation(logger, "clear_pattern", pattern, deleted=deleted)
243
+ return deleted
244
+ else:
245
+ # Memory cache pattern matching
246
+ keys_to_delete = [k for k in self.memory_cache.keys() if pattern.replace("*", "") in k]
247
+ for key in keys_to_delete:
248
+ del self.memory_cache[key]
249
+ log_cache_operation(logger, "clear_pattern", pattern, deleted=len(keys_to_delete))
250
+ return len(keys_to_delete)
251
+
252
+ except Exception as e:
253
+ logger.error("Cache clear pattern failed", pattern=pattern, error=str(e))
254
+ return 0
255
+
256
+ # ============================================================================
257
+ # API Key Specific Cache Methods
258
+ # ============================================================================
259
+
260
+ async def cache_api_key(self, provider: str, session_id: str, key_data: Dict[str, Any]) -> bool:
261
+ """Cache API key data."""
262
+ cache_key = f"api_key:{provider}:{session_id}"
263
+ return await self.set(cache_key, key_data, self.settings.api_key_cache_ttl)
264
+
265
+ async def get_cached_api_key(self, provider: str, session_id: str) -> Optional[Dict[str, Any]]:
266
+ """Get cached API key data."""
267
+ cache_key = f"api_key:{provider}:{session_id}"
268
+ return await self.get(cache_key)
269
+
270
+ async def remove_cached_api_key(self, provider: str, session_id: str) -> bool:
271
+ """Remove cached API key."""
272
+ cache_key = f"api_key:{provider}:{session_id}"
273
+ return await self.delete(cache_key)
274
+
275
+ async def cache_models(self, provider: str, models: List[str]) -> bool:
276
+ """Cache available models for provider."""
277
+ cache_key = f"models:{provider}"
278
+ return await self.set(cache_key, {"models": models, "cached_at": "now"},
279
+ ttl=3600) # 1 hour
280
+
281
+ async def get_cached_models(self, provider: str) -> Optional[List[str]]:
282
+ """Get cached models for provider."""
283
+ cache_key = f"models:{provider}"
284
+ data = await self.get(cache_key)
285
+ return data.get("models") if data else None
286
+
287
+ # ============================================================================
288
+ # Redis Streams Methods for Event Waiting
289
+ # ============================================================================
290
+
291
+ async def stream_add(self, stream: str, data: Dict[str, Any], maxlen: int = 1000) -> Optional[str]:
292
+ """Add message to Redis Stream.
293
+
294
+ Args:
295
+ stream: Stream name (e.g., 'events:whatsapp_message_received')
296
+ data: Event data to store
297
+ maxlen: Maximum stream length (approximate, uses ~)
298
+
299
+ Returns:
300
+ Message ID if successful, None otherwise
301
+ """
302
+ try:
303
+ if self.use_redis and self.redis and self._streams_available:
304
+ # Serialize ALL values with json.dumps to preserve types
305
+ # This matches the pattern used in set() and ensures proper round-trip:
306
+ # - json.dumps(True) → "true" (lowercase, valid JSON)
307
+ # - json.loads("true") → True (Python bool)
308
+ # Using str() would break: str(True) → "True" → json.loads fails
309
+ serialized = {k: json.dumps(v, default=str) for k, v in data.items()}
310
+ msg_id = await self.redis.xadd(stream, serialized, maxlen=maxlen, approximate=True)
311
+ logger.debug(f"Stream add: {stream} -> {msg_id}")
312
+ return msg_id
313
+ return None
314
+ except Exception as e:
315
+ logger.error(f"Stream add failed: {stream}", error=str(e))
316
+ return None
317
+
318
+ async def stream_read(
319
+ self,
320
+ streams: Dict[str, str],
321
+ count: int = 1,
322
+ block: Optional[int] = None
323
+ ) -> Optional[List[Any]]:
324
+ """Read from Redis Streams.
325
+
326
+ Args:
327
+ streams: Dict of stream_name -> last_id (use '$' for new messages only, '0' for all)
328
+ count: Maximum number of messages to read
329
+ block: Milliseconds to block (None = no blocking, 0 = infinite)
330
+
331
+ Returns:
332
+ List of [stream_name, [(msg_id, data), ...]] or None
333
+ """
334
+ try:
335
+ if self.use_redis and self.redis:
336
+ result = await self.redis.xread(streams, count=count, block=block)
337
+ return result
338
+ return None
339
+ except Exception as e:
340
+ logger.error(f"Stream read failed: {streams.keys()}", error=str(e))
341
+ return None
342
+
343
+ async def stream_create_group(
344
+ self,
345
+ stream: str,
346
+ group: str,
347
+ start_id: str = '$'
348
+ ) -> bool:
349
+ """Create consumer group for stream.
350
+
351
+ Args:
352
+ stream: Stream name
353
+ group: Consumer group name
354
+ start_id: Start reading from ('$' = new only, '0' = all)
355
+
356
+ Returns:
357
+ True if created or already exists
358
+ """
359
+ try:
360
+ if self.use_redis and self.redis and self._streams_available:
361
+ try:
362
+ await self.redis.xgroup_create(stream, group, start_id, mkstream=True)
363
+ logger.info(f"Created consumer group: {group} on {stream}")
364
+ return True
365
+ except Exception as e:
366
+ if "BUSYGROUP" in str(e):
367
+ # Group already exists - this is fine
368
+ return True
369
+ raise
370
+ return False
371
+ except Exception as e:
372
+ logger.error(f"Stream create group failed: {stream}/{group}", error=str(e))
373
+ return False
374
+
375
+ async def stream_read_group(
376
+ self,
377
+ group: str,
378
+ consumer: str,
379
+ streams: Dict[str, str],
380
+ count: int = 1,
381
+ block: Optional[int] = None
382
+ ) -> Optional[List[Any]]:
383
+ """Read from streams using consumer group.
384
+
385
+ Args:
386
+ group: Consumer group name
387
+ consumer: Consumer name (unique per worker)
388
+ streams: Dict of stream_name -> last_id (use '>' for new pending messages)
389
+ count: Maximum messages to read
390
+ block: Milliseconds to block
391
+
392
+ Returns:
393
+ List of messages or None
394
+ """
395
+ try:
396
+ if self.use_redis and self.redis and self._streams_available:
397
+ result = await self.redis.xreadgroup(
398
+ group, consumer, streams,
399
+ count=count, block=block
400
+ )
401
+ return result
402
+ return None
403
+ except Exception as e:
404
+ error_str = str(e).lower()
405
+ # Timeout errors are expected during blocking reads - log at debug level
406
+ if "timeout" in error_str or "timed out" in error_str:
407
+ logger.debug(f"Stream read group timeout: {group}/{consumer}", error=str(e))
408
+ else:
409
+ logger.error(f"Stream read group failed: {group}/{consumer}", error=str(e))
410
+ return None
411
+
412
+ async def stream_ack(self, stream: str, group: str, *msg_ids: str) -> int:
413
+ """Acknowledge messages in consumer group.
414
+
415
+ Args:
416
+ stream: Stream name
417
+ group: Consumer group name
418
+ msg_ids: Message IDs to acknowledge
419
+
420
+ Returns:
421
+ Number of messages acknowledged
422
+ """
423
+ try:
424
+ if self.use_redis and self.redis:
425
+ count = await self.redis.xack(stream, group, *msg_ids)
426
+ return count
427
+ return 0
428
+ except Exception as e:
429
+ logger.error(f"Stream ack failed: {stream}/{group}", error=str(e))
430
+ return 0
431
+
432
+ async def stream_delete(self, stream: str, *msg_ids: str) -> int:
433
+ """Delete messages from stream.
434
+
435
+ Args:
436
+ stream: Stream name
437
+ msg_ids: Message IDs to delete
438
+
439
+ Returns:
440
+ Number of messages deleted
441
+ """
442
+ try:
443
+ if self.use_redis and self.redis:
444
+ count = await self.redis.xdel(stream, *msg_ids)
445
+ return count
446
+ return 0
447
+ except Exception as e:
448
+ logger.error(f"Stream delete failed: {stream}", error=str(e))
449
+ return 0
450
+
451
+ def is_redis_available(self) -> bool:
452
+ """Check if Redis is available and connected."""
453
+ return self.use_redis and self.redis is not None
454
+
455
+ def is_streams_available(self) -> bool:
456
+ """Check if Redis Streams are available (for trigger nodes).
457
+
458
+ Returns True only if Redis is connected AND supports Streams commands.
459
+ This is checked once during startup to avoid runtime failures.
460
+ """
461
+ return self.use_redis and self.redis is not None and self._streams_available
@@ -0,0 +1,128 @@
1
+ """Environment-driven configuration with Pydantic v2."""
2
+
3
+ from typing import List, Literal, Optional
4
+ from pathlib import Path
5
+ from pydantic import Field, field_validator
6
+ from pydantic_settings import BaseSettings
7
+
8
+
9
+ class Settings(BaseSettings):
10
+ """Application settings driven entirely by environment variables."""
11
+
12
+ # Service Ports (used by start.js, vite, and docker-compose, not hardcoded here)
13
+ vite_client_port: Optional[int] = Field(default=None, env="VITE_CLIENT_PORT")
14
+ python_backend_port: Optional[int] = Field(default=None, env="PYTHON_BACKEND_PORT")
15
+ whatsapp_rpc_port: Optional[int] = Field(default=None, env="WHATSAPP_RPC_PORT")
16
+ redis_port: Optional[int] = Field(default=None, env="REDIS_PORT")
17
+
18
+ # Server Configuration
19
+ host: str = Field(env="HOST")
20
+ port: int = Field(env="PORT", ge=1024, le=65535)
21
+ debug: bool = Field(default=False, env="DEBUG")
22
+ workers: int = Field(default=1, env="WORKERS", ge=1, le=8)
23
+
24
+ # Authentication
25
+ auth_mode: Literal["single", "multi"] = Field(default="single", env="AUTH_MODE")
26
+ jwt_secret_key: str = Field(env="JWT_SECRET_KEY", min_length=32)
27
+ jwt_expire_minutes: int = Field(default=10080, env="JWT_EXPIRE_MINUTES", ge=60) # 7 days
28
+ jwt_cookie_name: str = Field(default="machina_token", env="JWT_COOKIE_NAME")
29
+ jwt_cookie_secure: bool = Field(default=False, env="JWT_COOKIE_SECURE") # True in production
30
+ jwt_cookie_samesite: Literal["lax", "strict", "none"] = Field(default="lax", env="JWT_COOKIE_SAMESITE")
31
+
32
+ # Security
33
+ secret_key: str = Field(env="SECRET_KEY", min_length=32)
34
+ cors_origins: List[str] = Field(env="CORS_ORIGINS")
35
+
36
+ # Database Configuration
37
+ database_url: str = Field(env="DATABASE_URL")
38
+ database_echo: bool = Field(default=False, env="DATABASE_ECHO")
39
+ database_pool_size: int = Field(default=20, env="DATABASE_POOL_SIZE", ge=5, le=100)
40
+ database_max_overflow: int = Field(default=30, env="DATABASE_MAX_OVERFLOW", ge=10, le=100)
41
+
42
+ # Cache Configuration
43
+ redis_url: Optional[str] = Field(default=None, env="REDIS_URL")
44
+ redis_enabled: bool = Field(default=False, env="REDIS_ENABLED")
45
+ cache_ttl: int = Field(default=3600, env="CACHE_TTL", ge=60)
46
+
47
+ # Execution Engine
48
+ dlq_enabled: bool = Field(default=False, env="DLQ_ENABLED")
49
+
50
+ # Temporal Configuration
51
+ temporal_enabled: bool = Field(default=False, env="TEMPORAL_ENABLED")
52
+ temporal_server_address: str = Field(default="localhost:7233", env="TEMPORAL_SERVER_ADDRESS")
53
+ temporal_namespace: str = Field(default="default", env="TEMPORAL_NAMESPACE")
54
+ temporal_task_queue: str = Field(default="machina-tasks", env="TEMPORAL_TASK_QUEUE")
55
+
56
+ # API Keys (all optional, injected at runtime)
57
+ google_maps_api_key: Optional[str] = Field(default=None, env="GOOGLE_MAPS_API_KEY")
58
+ openai_api_key: Optional[str] = Field(default=None, env="OPENAI_API_KEY")
59
+ anthropic_api_key: Optional[str] = Field(default=None, env="ANTHROPIC_API_KEY")
60
+ google_ai_api_key: Optional[str] = Field(default=None, env="GOOGLE_AI_API_KEY")
61
+
62
+ # WhatsApp Service URL (Flask service)
63
+ whatsapp_service_url: str = Field(default="http://localhost:5000", env="WHATSAPP_SERVICE_URL")
64
+
65
+ # WebSocket Configuration
66
+ websocket_url: str = Field(default="", env="WEBSOCKET_URL")
67
+ websocket_api_key: Optional[str] = Field(default=None, env="WEBSOCKET_API_KEY")
68
+
69
+ # Android Relay Configuration (passed to Vite frontend)
70
+ vite_android_relay_url: Optional[str] = Field(default=None, env="VITE_ANDROID_RELAY_URL")
71
+
72
+ # Frontend Auth Configuration (passed to Vite frontend)
73
+ vite_auth_enabled: Optional[str] = Field(default=None, env="VITE_AUTH_ENABLED")
74
+
75
+ # API Key Security
76
+ api_key_encryption_key: str = Field(env="API_KEY_ENCRYPTION_KEY", min_length=32)
77
+ api_key_cache_ttl: int = Field(default=2592000, env="API_KEY_CACHE_TTL", ge=3600)
78
+
79
+ # Logging
80
+ log_level: str = Field(default="INFO", env="LOG_LEVEL")
81
+ log_format: str = Field(default="json", env="LOG_FORMAT")
82
+ log_file: Optional[str] = Field(default=None, env="LOG_FILE")
83
+
84
+ # Rate Limiting
85
+ rate_limit_enabled: bool = Field(default=True, env="RATE_LIMIT_ENABLED")
86
+ rate_limit_requests: int = Field(default=100, env="RATE_LIMIT_REQUESTS", ge=10)
87
+ rate_limit_window: int = Field(default=60, env="RATE_LIMIT_WINDOW", ge=10)
88
+
89
+ # Service Timeouts
90
+ ai_timeout: int = Field(default=30, env="AI_TIMEOUT", ge=5, le=300)
91
+ ai_max_retries: int = Field(default=3, env="AI_MAX_RETRIES", ge=0, le=5)
92
+ ai_retry_delay: float = Field(default=1.0, env="AI_RETRY_DELAY", ge=0.1, le=10.0)
93
+
94
+ maps_timeout: int = Field(default=10, env="MAPS_TIMEOUT", ge=5, le=60)
95
+ maps_max_requests_per_second: int = Field(default=50, env="MAPS_MAX_RPS", ge=1, le=1000)
96
+
97
+ # Health Check
98
+ health_check_interval: int = Field(default=30, env="HEALTH_CHECK_INTERVAL", ge=10)
99
+
100
+ @field_validator("database_url")
101
+ @classmethod
102
+ def validate_database_url(cls, v):
103
+ """Ensure database directory exists for SQLite."""
104
+ if v and v.startswith("sqlite"):
105
+ if ":///" in v:
106
+ db_path = v.split("///")[1]
107
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
108
+ return v
109
+
110
+
111
+ @property
112
+ def is_development(self) -> bool:
113
+ """Check if running in development mode."""
114
+ return self.debug
115
+
116
+ @property
117
+ def is_production(self) -> bool:
118
+ """Check if running in production mode."""
119
+ return not self.debug
120
+
121
+ model_config = {
122
+ "env_file": "../.env",
123
+ "env_file_encoding": "utf-8",
124
+ "case_sensitive": False,
125
+ "extra": "forbid",
126
+ "env_parse_none_str": "none",
127
+ "env_nested_delimiter": "__",
128
+ }
@@ -0,0 +1,99 @@
1
+ """Dependency injection container for the application."""
2
+
3
+ from dependency_injector import containers, providers
4
+ from dependency_injector.wiring import Provide, inject
5
+
6
+ from core.config import Settings
7
+ from core.database import Database
8
+ from core.cache import CacheService
9
+ from services.ai import AIService
10
+ from services.maps import MapsService
11
+ from services.workflow import WorkflowService
12
+ from services.auth import AuthService
13
+ from services.text import TextService
14
+ from services.android_service import AndroidService
15
+ from services.user_auth import UserAuthService
16
+ from services.temporal import TemporalClientWrapper
17
+
18
+
19
+ class Container(containers.DeclarativeContainer):
20
+ """Application dependency injection container."""
21
+
22
+ # Configuration
23
+ config = providers.Configuration()
24
+
25
+ # Settings
26
+ settings = providers.Singleton(
27
+ Settings,
28
+ )
29
+
30
+ # Database (needed by CacheService for SQLite fallback)
31
+ database = providers.Singleton(
32
+ Database,
33
+ settings=settings
34
+ )
35
+
36
+ # Cache service (uses Redis when available, SQLite otherwise)
37
+ cache = providers.Singleton(
38
+ CacheService,
39
+ settings=settings,
40
+ database=database
41
+ )
42
+
43
+ # Temporal client (lazy - only created when temporal_enabled)
44
+ temporal_client = providers.Singleton(
45
+ TemporalClientWrapper,
46
+ server_address=settings.provided.temporal_server_address,
47
+ namespace=settings.provided.temporal_namespace,
48
+ )
49
+
50
+ # Services
51
+ auth_service = providers.Factory(
52
+ AuthService,
53
+ cache=cache,
54
+ database=database,
55
+ settings=settings
56
+ )
57
+
58
+ user_auth_service = providers.Factory(
59
+ UserAuthService,
60
+ database=database,
61
+ settings=settings
62
+ )
63
+
64
+ ai_service = providers.Factory(
65
+ AIService,
66
+ auth_service=auth_service,
67
+ database=database,
68
+ cache=cache,
69
+ settings=settings
70
+ )
71
+
72
+ maps_service = providers.Factory(
73
+ MapsService,
74
+ auth_service=auth_service,
75
+ settings=settings
76
+ )
77
+
78
+ text_service = providers.Factory(
79
+ TextService
80
+ )
81
+
82
+ android_service = providers.Factory(
83
+ AndroidService
84
+ )
85
+
86
+ workflow_service = providers.Singleton(
87
+ WorkflowService,
88
+ database=database,
89
+ ai_service=ai_service,
90
+ maps_service=maps_service,
91
+ text_service=text_service,
92
+ android_service=android_service,
93
+ cache=cache,
94
+ settings=settings
95
+ )
96
+
97
+
98
+ # Global container instance
99
+ container = Container()