quantum-framework 0.18.0__py3-none-any.whl

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 (333) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/admin.py +131 -0
  4. quantum/cli/check.py +223 -0
  5. quantum/cli/commands/__init__.py +15 -0
  6. quantum/cli/commands/build.py +414 -0
  7. quantum/cli/commands/dev.py +185 -0
  8. quantum/cli/commands/docs.py +329 -0
  9. quantum/cli/commands/lint.py +523 -0
  10. quantum/cli/commands/migrate.py +548 -0
  11. quantum/cli/commands/new.py +622 -0
  12. quantum/cli/commands/serve.py +190 -0
  13. quantum/cli/commands/test.py +193 -0
  14. quantum/cli/deploy.py +810 -0
  15. quantum/cli/hot_reload.py +951 -0
  16. quantum/cli/jobs.py +356 -0
  17. quantum/cli/mq.py +582 -0
  18. quantum/cli/pkg.py +390 -0
  19. quantum/cli/runner.py +565 -0
  20. quantum/cli/server_process.py +159 -0
  21. quantum/cli/utils.py +334 -0
  22. quantum/compiler/__init__.py +30 -0
  23. quantum/compiler/base_generator.py +367 -0
  24. quantum/compiler/cli.py +295 -0
  25. quantum/compiler/expression_transformer.py +444 -0
  26. quantum/compiler/javascript/__init__.py +10 -0
  27. quantum/compiler/javascript/generator.py +659 -0
  28. quantum/compiler/optimizer.py +270 -0
  29. quantum/compiler/python/__init__.py +10 -0
  30. quantum/compiler/python/generator.py +883 -0
  31. quantum/compiler/python/runtime.py +863 -0
  32. quantum/compiler/transpiler.py +330 -0
  33. quantum/core/__init__.py +3 -0
  34. quantum/core/ast_nodes.py +2619 -0
  35. quantum/core/config_env.py +54 -0
  36. quantum/core/docs_links.py +8 -0
  37. quantum/core/expression_diagnostics.py +87 -0
  38. quantum/core/expression_stdlib.py +208 -0
  39. quantum/core/expressions.py +677 -0
  40. quantum/core/features/agents/src/__init__.py +30 -0
  41. quantum/core/features/agents/src/ast_node.py +540 -0
  42. quantum/core/features/conditionals/src/__init__.py +8 -0
  43. quantum/core/features/conditionals/src/ast_node.py +68 -0
  44. quantum/core/features/data_import/src/__init__.py +0 -0
  45. quantum/core/features/data_import/src/ast_node.py +292 -0
  46. quantum/core/features/data_import/src/runtime.py +533 -0
  47. quantum/core/features/dump/src/__init__.py +12 -0
  48. quantum/core/features/dump/src/ast_node.py +106 -0
  49. quantum/core/features/dump/src/parser.py +61 -0
  50. quantum/core/features/dump/src/runtime.py +246 -0
  51. quantum/core/features/functions/src/__init__.py +8 -0
  52. quantum/core/features/functions/src/ast_node.py +149 -0
  53. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  54. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  55. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  56. quantum/core/features/invocation/src/__init__.py +0 -0
  57. quantum/core/features/invocation/src/ast_node.py +147 -0
  58. quantum/core/features/invocation/src/runtime.py +332 -0
  59. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  60. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  61. quantum/core/features/knowledge_base/src/parser.py +82 -0
  62. quantum/core/features/logging/src/__init__.py +12 -0
  63. quantum/core/features/logging/src/ast_node.py +111 -0
  64. quantum/core/features/logging/src/parser.py +50 -0
  65. quantum/core/features/logging/src/runtime.py +190 -0
  66. quantum/core/features/loops/src/__init__.py +8 -0
  67. quantum/core/features/loops/src/ast_node.py +60 -0
  68. quantum/core/features/query/src/__init__.py +0 -0
  69. quantum/core/features/query/src/query_validators.py +20 -0
  70. quantum/core/features/state_management/src/__init__.py +11 -0
  71. quantum/core/features/state_management/src/ast_node.py +121 -0
  72. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  73. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  74. quantum/core/features/terminal_engine/src/parser.py +361 -0
  75. quantum/core/features/testing_engine/src/__init__.py +41 -0
  76. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  77. quantum/core/features/testing_engine/src/parser.py +604 -0
  78. quantum/core/features/theming/src/__init__.py +48 -0
  79. quantum/core/features/theming/src/ast_node.py +137 -0
  80. quantum/core/features/theming/src/presets.py +405 -0
  81. quantum/core/features/ui_engine/src/__init__.py +20 -0
  82. quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
  83. quantum/core/features/ui_engine/src/parser.py +1192 -0
  84. quantum/core/features/websocket/src/__init__.py +24 -0
  85. quantum/core/features/websocket/src/ast_node.py +247 -0
  86. quantum/core/guards.py +53 -0
  87. quantum/core/html_compat.py +299 -0
  88. quantum/core/parser.py +1551 -0
  89. quantum/core/parser_registry.py +213 -0
  90. quantum/core/parsers/__init__.py +76 -0
  91. quantum/core/parsers/ai/__init__.py +12 -0
  92. quantum/core/parsers/ai/agent_parser.py +106 -0
  93. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  94. quantum/core/parsers/ai/llm_parser.py +78 -0
  95. quantum/core/parsers/ai/team_parser.py +88 -0
  96. quantum/core/parsers/base.py +322 -0
  97. quantum/core/parsers/composition/__init__.py +10 -0
  98. quantum/core/parsers/composition/import_parser.py +50 -0
  99. quantum/core/parsers/composition/slot_parser.py +48 -0
  100. quantum/core/parsers/control_flow/__init__.py +11 -0
  101. quantum/core/parsers/control_flow/if_parser.py +71 -0
  102. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  103. quantum/core/parsers/control_flow/set_parser.py +93 -0
  104. quantum/core/parsers/data/__init__.py +12 -0
  105. quantum/core/parsers/data/data_parser.py +229 -0
  106. quantum/core/parsers/data/invoke_parser.py +133 -0
  107. quantum/core/parsers/data/query_parser.py +213 -0
  108. quantum/core/parsers/data/transaction_parser.py +86 -0
  109. quantum/core/parsers/events/__init__.py +9 -0
  110. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  111. quantum/core/parsers/forms/__init__.py +11 -0
  112. quantum/core/parsers/forms/action_parser.py +79 -0
  113. quantum/core/parsers/forms/flash_parser.py +34 -0
  114. quantum/core/parsers/forms/redirect_parser.py +33 -0
  115. quantum/core/parsers/functions/__init__.py +11 -0
  116. quantum/core/parsers/functions/function_parser.py +98 -0
  117. quantum/core/parsers/functions/param_parser.py +73 -0
  118. quantum/core/parsers/functions/return_parser.py +31 -0
  119. quantum/core/parsers/html/__init__.py +10 -0
  120. quantum/core/parsers/html/component_call_parser.py +130 -0
  121. quantum/core/parsers/html/html_parser.py +115 -0
  122. quantum/core/parsers/jobs/__init__.py +11 -0
  123. quantum/core/parsers/jobs/job_parser.py +71 -0
  124. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  125. quantum/core/parsers/jobs/thread_parser.py +57 -0
  126. quantum/core/parsers/messaging/__init__.py +17 -0
  127. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  128. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_parser.py +114 -0
  130. quantum/core/parsers/messaging/queue_parser.py +61 -0
  131. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  132. quantum/core/parsers/routing/__init__.py +9 -0
  133. quantum/core/parsers/routing/route_parser.py +41 -0
  134. quantum/core/parsers/scripting/__init__.py +12 -0
  135. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  136. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  137. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  138. quantum/core/parsers/scripting/python_parser.py +49 -0
  139. quantum/core/parsers/services/__init__.py +12 -0
  140. quantum/core/parsers/services/dump_parser.py +52 -0
  141. quantum/core/parsers/services/file_parser.py +48 -0
  142. quantum/core/parsers/services/log_parser.py +46 -0
  143. quantum/core/parsers/services/mail_parser.py +65 -0
  144. quantum/core/tiers.py +164 -0
  145. quantum/core/xml_lines.py +81 -0
  146. quantum/packages/__init__.py +28 -0
  147. quantum/packages/manager.py +413 -0
  148. quantum/packages/manifest.py +351 -0
  149. quantum/packages/registry.py +399 -0
  150. quantum/packages/resolver.py +336 -0
  151. quantum/plugins/__init__.py +33 -0
  152. quantum/plugins/hooks.py +329 -0
  153. quantum/plugins/loader.py +479 -0
  154. quantum/plugins/manifest.py +336 -0
  155. quantum/plugins/registry.py +371 -0
  156. quantum/runtime/__init__.py +25 -0
  157. quantum/runtime/action_handler.py +532 -0
  158. quantum/runtime/adapters/__init__.py +88 -0
  159. quantum/runtime/adapters/memory_adapter.py +690 -0
  160. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  161. quantum/runtime/adapters/redis_adapter.py +582 -0
  162. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  163. quantum/runtime/agent_service.py +1133 -0
  164. quantum/runtime/ast_cache.py +506 -0
  165. quantum/runtime/auth_service.py +267 -0
  166. quantum/runtime/component.py +1048 -0
  167. quantum/runtime/component_composer.py +133 -0
  168. quantum/runtime/component_resolver.py +174 -0
  169. quantum/runtime/conditions.py +54 -0
  170. quantum/runtime/database_service.py +595 -0
  171. quantum/runtime/db_schema.py +100 -0
  172. quantum/runtime/dev_panel.py +178 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +283 -0
  176. quantum/runtime/executor_registry.py +178 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +264 -0
  184. quantum/runtime/executors/control_flow/__init__.py +12 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +98 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +308 -0
  187. quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
  188. quantum/runtime/executors/control_flow/set_executor.py +464 -0
  189. quantum/runtime/executors/data/__init__.py +12 -0
  190. quantum/runtime/executors/data/data_executor.py +156 -0
  191. quantum/runtime/executors/data/invoke_executor.py +228 -0
  192. quantum/runtime/executors/data/query_executor.py +290 -0
  193. quantum/runtime/executors/data/transaction_executor.py +91 -0
  194. quantum/runtime/executors/jobs/__init__.py +11 -0
  195. quantum/runtime/executors/jobs/job_executor.py +190 -0
  196. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  197. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  198. quantum/runtime/executors/messaging/__init__.py +17 -0
  199. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  200. quantum/runtime/executors/messaging/message_executor.py +174 -0
  201. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  202. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  203. quantum/runtime/executors/scripting/__init__.py +11 -0
  204. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  205. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  206. quantum/runtime/executors/scripting/python_executor.py +249 -0
  207. quantum/runtime/executors/services/__init__.py +12 -0
  208. quantum/runtime/executors/services/dump_executor.py +72 -0
  209. quantum/runtime/executors/services/file_executor.py +89 -0
  210. quantum/runtime/executors/services/log_executor.py +77 -0
  211. quantum/runtime/executors/services/mail_executor.py +81 -0
  212. quantum/runtime/expression_cache.py +498 -0
  213. quantum/runtime/file_upload_service.py +326 -0
  214. quantum/runtime/function_registry.py +118 -0
  215. quantum/runtime/game_builder.py +166 -0
  216. quantum/runtime/game_code_generator.py +2371 -0
  217. quantum/runtime/game_templates.py +2006 -0
  218. quantum/runtime/godot_code_generator.py +4681 -0
  219. quantum/runtime/godot_templates.py +1449 -0
  220. quantum/runtime/job_executor.py +1599 -0
  221. quantum/runtime/knowledge_service.py +546 -0
  222. quantum/runtime/llm_cache.py +100 -0
  223. quantum/runtime/llm_providers.py +704 -0
  224. quantum/runtime/llm_service.py +287 -0
  225. quantum/runtime/local_server.py +28 -0
  226. quantum/runtime/logging_setup.py +141 -0
  227. quantum/runtime/message_broker.py +364 -0
  228. quantum/runtime/message_queue_service.py +571 -0
  229. quantum/runtime/param_validation.py +218 -0
  230. quantum/runtime/pypy_compat.py +317 -0
  231. quantum/runtime/python_bridge.py +698 -0
  232. quantum/runtime/query_validators.py +304 -0
  233. quantum/runtime/renderer.py +734 -0
  234. quantum/runtime/service_container.py +470 -0
  235. quantum/runtime/table_params.py +163 -0
  236. quantum/runtime/terminal_builder.py +76 -0
  237. quantum/runtime/terminal_code_generator.py +607 -0
  238. quantum/runtime/terminal_templates.py +243 -0
  239. quantum/runtime/testing_builder.py +77 -0
  240. quantum/runtime/testing_code_generator.py +833 -0
  241. quantum/runtime/testing_templates.py +85 -0
  242. quantum/runtime/ui_builder.py +222 -0
  243. quantum/runtime/ui_console.py +405 -0
  244. quantum/runtime/ui_desktop.py +56 -0
  245. quantum/runtime/ui_form_rules.py +193 -0
  246. quantum/runtime/ui_html_adapter.py +2567 -0
  247. quantum/runtime/ui_html_templates.py +2106 -0
  248. quantum/runtime/ui_mobile_adapter.py +1832 -0
  249. quantum/runtime/ui_mobile_templates.py +1003 -0
  250. quantum/runtime/ui_page_renderer.py +392 -0
  251. quantum/runtime/ui_pager.py +73 -0
  252. quantum/runtime/ui_table_edit.py +139 -0
  253. quantum/runtime/ui_textual_adapter.py +1866 -0
  254. quantum/runtime/ui_textual_templates.py +45 -0
  255. quantum/runtime/ui_tokens.py +465 -0
  256. quantum/runtime/ui_validator.py +365 -0
  257. quantum/runtime/ui_view.py +221 -0
  258. quantum/runtime/validators.py +256 -0
  259. quantum/runtime/web_server.py +2131 -0
  260. quantum/runtime/websocket_adapter.py +501 -0
  261. quantum/runtime/websocket_service.py +585 -0
  262. quantum/runtime/websocket_transport.py +289 -0
  263. quantum/runtime/wsgi.py +101 -0
  264. quantum/services.py +109 -0
  265. quantum/utils/__init__.py +1 -0
  266. quantum_admin/__init__.py +4 -0
  267. quantum_admin/assets/quantum-admin-connectors.js +123 -0
  268. quantum_admin/assets/quantum-admin.css +2058 -0
  269. quantum_admin/assets/quantum-source-highlight.js +106 -0
  270. quantum_admin/backend/__init__.py +4 -0
  271. quantum_admin/backend/audit_service.py +346 -0
  272. quantum_admin/backend/auth_service.py +394 -0
  273. quantum_admin/backend/component_discovery.py +766 -0
  274. quantum_admin/backend/config_generator.py +586 -0
  275. quantum_admin/backend/connector_service.py +1016 -0
  276. quantum_admin/backend/crud.py +557 -0
  277. quantum_admin/backend/database.py +226 -0
  278. quantum_admin/backend/db_setup_service.py +193 -0
  279. quantum_admin/backend/deploy_service.py +698 -0
  280. quantum_admin/backend/docker_service.py +422 -0
  281. quantum_admin/backend/environment_service.py +418 -0
  282. quantum_admin/backend/git_service.py +370 -0
  283. quantum_admin/backend/health_service.py +390 -0
  284. quantum_admin/backend/job_service.py +347 -0
  285. quantum_admin/backend/log_service.py +331 -0
  286. quantum_admin/backend/main.py +14426 -0
  287. quantum_admin/backend/models.py +986 -0
  288. quantum_admin/backend/pipeline_service.py +812 -0
  289. quantum_admin/backend/resource_manager.py +1118 -0
  290. quantum_admin/backend/schemas.py +419 -0
  291. quantum_admin/backend/secret_manager.py +277 -0
  292. quantum_admin/backend/settings_service.py +488 -0
  293. quantum_admin/backend/template_service.py +710 -0
  294. quantum_admin/backend/test_execution_service.py +364 -0
  295. quantum_admin/backend/test_generator.py +782 -0
  296. quantum_admin/backend/webhook_service.py +454 -0
  297. quantum_admin/backend/websocket_manager.py +360 -0
  298. quantum_admin/components/admin/_layout/AdminShell.q +121 -0
  299. quantum_admin/components/admin/agents.q +107 -0
  300. quantum_admin/components/admin/app/[name].q +558 -0
  301. quantum_admin/components/admin/applications.q +160 -0
  302. quantum_admin/components/admin/component/[...path].q +205 -0
  303. quantum_admin/components/admin/components.q +59 -0
  304. quantum_admin/components/admin/connectors.q +283 -0
  305. quantum_admin/components/admin/dashboard.q +86 -0
  306. quantum_admin/components/admin/database.q +95 -0
  307. quantum_admin/components/admin/features.q +69 -0
  308. quantum_admin/components/admin/index.q +12 -0
  309. quantum_admin/components/admin/jobs.q +75 -0
  310. quantum_admin/components/admin/login.q +65 -0
  311. quantum_admin/components/admin/logout.q +11 -0
  312. quantum_admin/components/admin/settings.q +174 -0
  313. quantum_admin/components/admin/source.q +45 -0
  314. quantum_admin/components/admin/tests.q +73 -0
  315. quantum_admin/run.py +13 -0
  316. quantum_admin/services/__init__.py +16 -0
  317. quantum_admin/services/_base.py +84 -0
  318. quantum_admin/services/_component_test_generator.py +205 -0
  319. quantum_admin/services/_yaml_editor.py +82 -0
  320. quantum_admin/services/apps.py +330 -0
  321. quantum_admin/services/auth.py +61 -0
  322. quantum_admin/services/components.py +210 -0
  323. quantum_admin/services/connectors.py +164 -0
  324. quantum_admin/services/projects.py +189 -0
  325. quantum_admin/services/repository.py +223 -0
  326. quantum_admin/services/settings.py +150 -0
  327. quantum_admin/services/yaml_import.py +118 -0
  328. quantum_framework-0.18.0.dist-info/METADATA +281 -0
  329. quantum_framework-0.18.0.dist-info/RECORD +333 -0
  330. quantum_framework-0.18.0.dist-info/WHEEL +5 -0
  331. quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
  332. quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
  333. quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1048 @@
1
+ """
2
+ Quantum Component Runtime - Execute Quantum components
3
+ """
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List
8
+
9
+ # Fix imports
10
+
11
+ from quantum.core.ast_nodes import (
12
+ ComponentNode, QuantumReturn, DispatchEventNode, OnEventNode, QueryNode,
13
+ DataNode, FileNode, MailNode, TransactionNode, LLMNode, LLMMessageNode,
14
+ JobNode, ScheduleNode, ThreadNode, # Job Execution System
15
+ # Message Queue System
16
+ MessageNode, SubscribeNode, QueueNode, MessageAckNode, MessageNackNode,
17
+ # Python Scripting System
18
+ PythonNode, PyImportNode, PyClassNode, PyDecoratorNode, PyExprNode,
19
+ # Render-only nodes (handled by renderer, not executors)
20
+ HTMLNode, TextNode, DocTypeNode,
21
+ # Special-purpose nodes (handled outside executor dispatch)
22
+ ActionNode, RedirectNode, FlashNode, CommentNode, ImportNode, SlotNode,
23
+ ComponentCallNode,
24
+ )
25
+ from quantum.core.expression_diagnostics import (
26
+ is_absent_scope, looks_like_json_object, report_unresolved, root_scope,
27
+ )
28
+ from quantum.core.expressions import (
29
+ ExpressionEvaluator, ExpressionError, UndefinedError, is_regex_quantifier,
30
+ )
31
+ from quantum.core.features.conditionals.src.ast_node import IfNode
32
+ from quantum.core.features.loops.src.ast_node import LoopNode
33
+ from quantum.core.features.state_management.src.ast_node import SetNode
34
+ from quantum.core.features.functions.src.ast_node import FunctionNode
35
+ from quantum.core.features.invocation.src.ast_node import InvokeNode
36
+ from quantum.core.features.knowledge_base.src.ast_node import KnowledgeNode
37
+ from quantum.core.features.agents.src.ast_node import AgentNode, AgentTeamNode
38
+ from quantum.core.features.websocket.src import (
39
+ WebSocketNode, WebSocketHandlerNode, WebSocketSendNode, WebSocketCloseNode
40
+ )
41
+ from quantum.core.features.logging.src import LogNode, LoggingService
42
+ from quantum.core.features.dump.src import DumpNode, DumpService
43
+ from quantum.runtime.database_service import DatabaseService, QueryResult
44
+ from quantum.runtime.query_validators import QueryValidator, QueryValidationError
45
+ from quantum.runtime.execution_context import ExecutionContext, VariableNotFoundError
46
+ from quantum.runtime import param_validation
47
+ from quantum.runtime.validators import QuantumValidators, ValidationError
48
+ from quantum.runtime.function_registry import FunctionRegistry
49
+ from quantum.core.features.invocation.src.runtime import InvocationService
50
+ from quantum.core.features.data_import.src.runtime import DataImportService
51
+ from quantum.runtime.file_upload_service import FileUploadService, FileUploadError
52
+ from quantum.runtime.email_service import EmailService, EmailError
53
+ from quantum.runtime.llm_service import LLMService, LLMError
54
+ from quantum.runtime.knowledge_service import KnowledgeService, KnowledgeError
55
+ from quantum.runtime.agent_service import (
56
+ AgentService, AgentError, get_agent_service,
57
+ get_multi_agent_service, MultiAgentService, BUILTIN_TOOLS
58
+ )
59
+ from quantum.runtime.message_queue_service import MessageQueueService, MessageQueueError
60
+ from quantum.runtime.job_executor import (
61
+ JobExecutor, ScheduleService, ThreadService, JobQueueService,
62
+ JobExecutorError, ScheduleError, ThreadError, JobQueueError,
63
+ parse_duration
64
+ )
65
+ from quantum.runtime.expression_cache import get_databinding_cache, DataBindingCache
66
+ from quantum.runtime.executor_registry import ExecutorRegistry
67
+ from quantum.runtime.executors.control_flow.loop_executor import (
68
+ as_return_value, produced_return,
69
+ )
70
+ from quantum.runtime.service_container import ServiceContainer
71
+ from quantum.runtime.conditions import evaluate_condition
72
+ from quantum.core.features.ui_engine.src.ast_nodes import is_ui_node
73
+ import re
74
+ import logging
75
+
76
+ # `session.userId`, `form.email` — a scoped reference, as opposed to an operation on one.
77
+ _PLAIN_REFERENCE = re.compile(r'^[A-Za-z_]\w*(\.\w+)*$')
78
+
79
+ logger = logging.getLogger(__name__)
80
+
81
+
82
+ class ComponentExecutionError(Exception):
83
+ """Error in component execution"""
84
+ pass
85
+
86
+
87
+ def _create_executor_registry(runtime: 'ComponentRuntime') -> ExecutorRegistry:
88
+ """
89
+ Create and populate the executor registry with all executors.
90
+
91
+ This factory function creates all modular executors and registers them.
92
+ The registry can then dispatch execution to the appropriate executor
93
+ based on node type.
94
+
95
+ Args:
96
+ runtime: ComponentRuntime instance for executor initialization
97
+
98
+ Returns:
99
+ Populated ExecutorRegistry
100
+ """
101
+ from quantum.runtime.executors import (
102
+ # Control flow
103
+ IfExecutor, LoopExecutor, SetExecutor, RedirectExecutor,
104
+ # Data
105
+ QueryExecutor, InvokeExecutor, DataExecutor, TransactionExecutor,
106
+ # Services
107
+ LogExecutor, DumpExecutor, FileExecutor, MailExecutor,
108
+ # AI
109
+ LLMExecutor, AgentExecutor, TeamExecutor, KnowledgeExecutor,
110
+ # Messaging
111
+ WebSocketExecutor, WebSocketSendExecutor, WebSocketCloseExecutor,
112
+ MessageExecutor, SubscribeExecutor, QueueExecutor,
113
+ MessageAckExecutor, MessageNackExecutor,
114
+ # Jobs
115
+ ScheduleExecutor, ThreadExecutor, JobExecutor,
116
+ # Scripting
117
+ PythonExecutor, PyImportExecutor, PyClassExecutor,
118
+ )
119
+
120
+ registry = ExecutorRegistry()
121
+
122
+ # Register all executors
123
+ executors = [
124
+ # Control flow
125
+ IfExecutor(runtime),
126
+ LoopExecutor(runtime),
127
+ SetExecutor(runtime),
128
+ RedirectExecutor(runtime),
129
+ # Data
130
+ QueryExecutor(runtime),
131
+ InvokeExecutor(runtime),
132
+ DataExecutor(runtime),
133
+ TransactionExecutor(runtime),
134
+ # Services
135
+ LogExecutor(runtime),
136
+ DumpExecutor(runtime),
137
+ FileExecutor(runtime),
138
+ MailExecutor(runtime),
139
+ # AI
140
+ LLMExecutor(runtime),
141
+ AgentExecutor(runtime),
142
+ TeamExecutor(runtime),
143
+ KnowledgeExecutor(runtime),
144
+ # Messaging
145
+ WebSocketExecutor(runtime),
146
+ WebSocketSendExecutor(runtime),
147
+ WebSocketCloseExecutor(runtime),
148
+ MessageExecutor(runtime),
149
+ SubscribeExecutor(runtime),
150
+ QueueExecutor(runtime),
151
+ MessageAckExecutor(runtime),
152
+ MessageNackExecutor(runtime),
153
+ # Jobs
154
+ ScheduleExecutor(runtime),
155
+ ThreadExecutor(runtime),
156
+ JobExecutor(runtime),
157
+ # Scripting
158
+ PythonExecutor(runtime),
159
+ PyImportExecutor(runtime),
160
+ PyClassExecutor(runtime),
161
+ ]
162
+
163
+ registry.register_all(executors)
164
+ logger.debug(f"Initialized executor registry with {registry.executor_count} executors")
165
+
166
+ return registry
167
+
168
+ class ComponentRuntime:
169
+ """Runtime for executing Quantum components"""
170
+
171
+ def __init__(self, config: Dict[str, Any] = None, use_modular_executors: bool = True):
172
+ self.execution_context = ExecutionContext()
173
+ # Keep self.context for backward compatibility
174
+ self.context: Dict[str, Any] = {}
175
+ # Function registry
176
+ self.function_registry = FunctionRegistry()
177
+ # Current component (for function resolution)
178
+ self.current_component: ComponentNode = None
179
+
180
+ # === NEW: Service Container for dependency injection ===
181
+ self._services = ServiceContainer(config)
182
+
183
+ # === NEW: Executor Registry for modular dispatch ===
184
+ self._use_modular_executors = use_modular_executors
185
+ self._executor_registry: ExecutorRegistry = None
186
+ if use_modular_executors:
187
+ self._executor_registry = _create_executor_registry(self)
188
+ else:
189
+ import warnings
190
+ warnings.warn(
191
+ "use_modular_executors=False is deprecated and will be removed in v2.0. "
192
+ "The modular ExecutorRegistry is now the default.",
193
+ DeprecationWarning,
194
+ stacklevel=2
195
+ )
196
+
197
+ # === LEGACY: Direct service references for backward compatibility ===
198
+ # Database service for query execution - pass local datasources from config
199
+ # The services (database_service, job_executor, llm_service...) are
200
+ # properties over self._services, defined below the class body.
201
+ #
202
+ # They used to be built here, eagerly, as a SECOND copy of what the
203
+ # ServiceContainer builds lazily. So every ComponentRuntime — every
204
+ # `quantum run hello.q` — created ./logs/ and ./quantum_jobs.db in the
205
+ # user's directory and started a thread pool, for a program that used
206
+ # none of them; and q:job (runtime.job_executor) and q:schedule
207
+ # (services.job_executor) ran on two different JobExecutors, while
208
+ # runtime.llm_service ignored llm.base_url (IA-1).
209
+ #
210
+ # The one expression evaluator (FRAMEWORK_PLAN.md Fase 2.1). Nothing
211
+ # else in this class compiles or executes an expression any more —
212
+ # ExpressionCache's compile()+eval() is no longer reachable from the
213
+ # runtime, which is the point.
214
+ # function_resolver makes the component's own q:function callable from
215
+ # an expression: {countElements(myArray)} used to resolve to nothing,
216
+ # leaving the placeholder in the text for the surrounding q:set to
217
+ # choke on ("could not convert string to float:
218
+ # '{sumNumbers(myNumbers)}'"). The registry fills up while the
219
+ # component executes, so it has to be a lookup, not a dict.
220
+ self._expressions = ExpressionEvaluator(
221
+ function_resolver=self._resolve_expression_function)
222
+ # Databinding cache for optimized {variable} interpolation (Phase 1 enhancement)
223
+ self._databinding_cache = get_databinding_cache()
224
+ # Pre-compiled regex patterns (avoid re-compilation on every call)
225
+ self._databinding_pattern = re.compile(r'\{([^}]+)\}')
226
+ self._order_by_pattern = re.compile(r'\s+ORDER\s+BY\s+[^;]+?(?=\s+(?:LIMIT|OFFSET|FOR\s+UPDATE)|$)', re.IGNORECASE)
227
+ self._limit_pattern = re.compile(r'\s+LIMIT\s+\d+', re.IGNORECASE)
228
+ self._offset_pattern = re.compile(r'\s+OFFSET\s+\d+', re.IGNORECASE)
229
+
230
+ @property
231
+ def services(self) -> ServiceContainer:
232
+ """Access to service container"""
233
+ return self._services
234
+
235
+ @property
236
+ def config(self) -> Dict[str, Any]:
237
+ """The project configuration this runtime runs with."""
238
+ return self._services._config or {}
239
+
240
+ @property
241
+ def executor_registry(self) -> ExecutorRegistry:
242
+ """Access to executor registry (may be None if not using modular executors)"""
243
+ return self._executor_registry
244
+
245
+ def execute_component(self, component: ComponentNode, params: Dict[str, Any] = None) -> Any:
246
+ """Execute a component and return the result"""
247
+ if params is None:
248
+ params = {}
249
+
250
+ self._enter(component, params)
251
+
252
+ # Validate parameters
253
+ validation_errors = self._validate_params(component, params)
254
+ if validation_errors:
255
+ raise ComponentExecutionError(f"Validation errors: {validation_errors}")
256
+
257
+ # Setup context - add params to both contexts
258
+ self.context.update(params)
259
+ for key, value in params.items():
260
+ self.execution_context.set_variable(key, value, scope="component")
261
+
262
+ # Execute component
263
+ try:
264
+ # Execute control flow statements first
265
+ for statement in component.statements:
266
+ # RET-1: the first q:return executed, in document order, ends
267
+ # the component. A top-level q:return used to be evaluated and
268
+ # discarded here, and only returns[0] was evaluated after every
269
+ # statement ran — so a q:if further down could override it.
270
+ if isinstance(statement, QuantumReturn):
271
+ return self._process_return_value(
272
+ statement.value, self.execution_context.get_all_variables())
273
+ result = self._execute_statement(statement, self.execution_context)
274
+ # Only a q:if whose branch ran a q:return, or a q:loop that
275
+ # collected at least one, ends the component here. q:set,
276
+ # q:query etc. produce values too, but those are not returns.
277
+ # The loop case used to be dropped: every q:return inside a
278
+ # q:loop was evaluated and thrown away, and the component
279
+ # returned None.
280
+ if produced_return(statement, result):
281
+ return as_return_value(result)
282
+
283
+ # For now, simple execution based on q:return
284
+ if component.returns:
285
+ first_return = component.returns[0]
286
+ # Use get_all_variables() for backward compatibility
287
+ return self._process_return_value(first_return.value, self.execution_context.get_all_variables())
288
+
289
+ return None
290
+
291
+ except Exception as e:
292
+ raise ComponentExecutionError(f"Execution error: {e}") from e
293
+
294
+ def _enter(self, component: ComponentNode, params: Dict[str, Any]) -> None:
295
+ """Make `component` current: its functions, and the scopes passed in params."""
296
+ self.current_component = component
297
+ self.function_registry.register_component(component)
298
+ # Phase F: Initialize scopes from special parameters before validation
299
+ if '_session_scope' in params:
300
+ self.execution_context.session_vars = params.pop('_session_scope')
301
+ if '_application_scope' in params:
302
+ self.execution_context.application_vars = params.pop('_application_scope')
303
+ if '_request_scope' in params:
304
+ self.execution_context.request_vars = params.pop('_request_scope')
305
+
306
+ def run_guards(self, component: ComponentNode, params: Dict[str, Any] = None) -> None:
307
+ """AUTH-6: run the page's guards before one of its actions.
308
+
309
+ Only the guards run — no other statement of the page — so a guard that
310
+ redirects raises PageRedirect and the action never starts.
311
+ """
312
+ from quantum.core.guards import page_guards
313
+
314
+ params = dict(params or {})
315
+ self._enter(component, params)
316
+ self.context.update(params)
317
+ for key, value in params.items():
318
+ self.execution_context.set_variable(key, value, scope="component")
319
+ for guard in page_guards(component):
320
+ self._execute_statement(guard, self.execution_context)
321
+
322
+ def _validate_params(self, component: ComponentNode, params: Dict[str, Any]) -> List[str]:
323
+ """Apply defaults, coerce types and check the rules on q:param.
324
+
325
+ This used to check `required` and nothing else. The parser collects
326
+ default, type, min, max, minlength, maxlength, pattern and enum from
327
+ every q:param — and all of it was discarded, so
328
+
329
+ <q:param name="idade" type="numeric" min="18" default="30" />
330
+
331
+ left `idade` undefined ("{idade} did not resolve"), accepted the
332
+ string "abc" as numeric, and accepted 7 for a minimum of 18. Every
333
+ attribute but `required` was decoration.
334
+
335
+ `params` is modified in place: defaults land in it, and values are
336
+ replaced by their coerced form so the component sees a number where
337
+ it asked for a number.
338
+ """
339
+ errors = []
340
+
341
+ # getattr, not component.params: execute_component also runs
342
+ # ApplicationNode (a q:application root), which declares no params and
343
+ # raised AttributeError here — seven of the UI example tests failed
344
+ # on this line, before reaching anything they were written to check.
345
+ for param_def in getattr(component, 'params', None) or []:
346
+ name = param_def.name
347
+ if not name:
348
+ continue
349
+
350
+ if name not in params or params[name] is None:
351
+ if param_def.default is not None:
352
+ params[name] = param_def.default
353
+ elif param_def.required:
354
+ errors.append(f"Required parameter '{name}' is missing")
355
+ continue
356
+ else:
357
+ continue
358
+
359
+ value, error = self._coerce_param(param_def, params[name])
360
+ if error:
361
+ errors.append(error)
362
+ continue
363
+ params[name] = value
364
+
365
+ errors.extend(self._check_param_rules(param_def, value))
366
+
367
+ return errors
368
+
369
+ def _coerce_param(self, param_def, value):
370
+ """Return (coerced_value, error). Only for types with one meaning.
371
+
372
+ Delega para quantum.runtime.param_validation: o ActionHandler tinha a
373
+ propria copia desta logica, com outra lista de tipos e outro
374
+ tratamento para arquivos, entao o mesmo q:param se comportava de dois
375
+ jeitos conforme estivesse num componente ou num q:action.
376
+ """
377
+ return param_validation.coerce(param_def, value)
378
+
379
+ def _check_param_rules(self, param_def, value) -> List[str]:
380
+ """min/max/minlength/maxlength/pattern/enum."""
381
+ return param_validation.check_rules(param_def, value)
382
+
383
+ def _process_return_value(self, value: str, context: Dict[str, Any] = None) -> Any:
384
+ """Process return value with databinding support"""
385
+ if not value:
386
+ return ""
387
+
388
+ if context is None:
389
+ context = {}
390
+
391
+ # Apply databinding first
392
+ processed_value = self._apply_databinding(value, context)
393
+
394
+ # If databinding returned a non-string (e.g., int, float, dict), return it as-is
395
+ if not isinstance(processed_value, str):
396
+ return processed_value
397
+
398
+ # RET-2: anything that is not a single {expression} is TEXT. The
399
+ # result used to be re-parsed after interpolation — as JSON when it
400
+ # started with { or [, as a number when it looked like one — so
401
+ # "{i}.{j}" came back as the float 1.2 and a literal "007" as 7 (a
402
+ # postcode "01310" would lose its zero).
403
+ return processed_value
404
+
405
+ def _execute_statement(self, statement, context):
406
+ """Execute a control flow statement"""
407
+ # Accept both ExecutionContext and Dict for backward compatibility
408
+ if isinstance(context, ExecutionContext):
409
+ exec_context = context
410
+ dict_context = context.get_all_variables()
411
+ else:
412
+ exec_context = self.execution_context
413
+ dict_context = context
414
+
415
+ # Skip render-only and special-purpose nodes
416
+ # HTMLNode/TextNode/DocTypeNode: handled by HTMLRenderer
417
+ # ActionNode/FlashNode: handled by ActionHandler (q:redirect outside an
418
+ # action has its own executor, ACT-7)
419
+ # ImportNode: handled at load-time by ComponentResolver
420
+ # SlotNode: handled by ComponentComposer
421
+ # FunctionNode: registered in FunctionRegistry before execution
422
+ # CommentNode: rendered as HTML comment by renderer
423
+ if isinstance(statement, (
424
+ HTMLNode, TextNode, DocTypeNode,
425
+ ActionNode, FlashNode,
426
+ CommentNode, ImportNode, SlotNode, FunctionNode,
427
+ # Resolved by HTMLRenderer._render_component_call during the render
428
+ # pass, exactly like HTMLNode. It was missing from this list, so
429
+ # every page using component composition — <Layout>, <Card> —
430
+ # raised "No modular executor registered for ComponentCallNode"
431
+ # and served a 500 the moment the legacy if-elif chain that used to
432
+ # swallow it was removed.
433
+ ComponentCallNode,
434
+ )) or is_ui_node(statement):
435
+ # ui:* inside a page is markup (UI-1): the renderer draws it.
436
+ return None
437
+
438
+ # <q:return> aninhado dentro de q:if / q:else / q:loop. Nao tem
439
+ # executor proprio: o nivel de componente coleta os returns a parte,
440
+ # entao um return dentro de um corpo chegava aqui e explodia com "No
441
+ # modular executor registered". Antes de o parser passar a reconhecer
442
+ # o aninhamento, ele nem chegava — era descartado em silencio, e
443
+ # `<q:if><q:return value="X"/></q:if>` simplesmente nao retornava X.
444
+ if isinstance(statement, QuantumReturn):
445
+ return self._process_return_value(statement.value, dict_context)
446
+
447
+ # === MODULAR EXECUTOR REGISTRY (no fallback) ===
448
+ if self._use_modular_executors and self._executor_registry:
449
+ if self._executor_registry.can_execute(statement):
450
+ return self._executor_registry.execute(statement, exec_context)
451
+ else:
452
+ # No executor registered for this node type
453
+ node_type = type(statement).__name__
454
+ raise ComponentExecutionError(f"No modular executor registered for {node_type}")
455
+
456
+ # If modular executors are disabled, raise an error
457
+ raise ComponentExecutionError("Modular executors are disabled but legacy fallback has been removed")
458
+
459
+ def _execute_body(self, statements: List, context: Dict[str, Any]):
460
+ """Execute a list of statements"""
461
+ for statement in statements:
462
+ if isinstance(statement, QuantumReturn):
463
+ return self._process_return_value(statement.value, context)
464
+ else:
465
+ # Delegate to _execute_statement for all other types
466
+ # (SetNode, IfNode, LoopNode, QueryNode, etc.)
467
+ result = self._execute_statement(statement, self.execution_context)
468
+ if produced_return(statement, result):
469
+ return as_return_value(result)
470
+ return None
471
+
472
+ def _evaluate_condition(self, condition: str, context: Dict[str, Any], guard: bool = False) -> bool:
473
+ """A condition by the language's rules — see quantum/runtime/conditions.py."""
474
+ return evaluate_condition(self._expressions, condition, context, guard=guard)
475
+
476
+ def _apply_databinding(self, text: str, context: Dict[str, Any]) -> Any:
477
+ """Apply variable databinding to text using {variable} syntax.
478
+
479
+ Optimized with pre-compiled regex pattern and DataBindingCache for
480
+ improved performance on repeated evaluations.
481
+ """
482
+ if not text:
483
+ return text
484
+
485
+ # Even if context is empty, we still need to process function calls
486
+ if context is None:
487
+ context = {}
488
+
489
+ # Use pre-compiled pattern (avoid re-compilation on every call)
490
+ pattern = self._databinding_pattern
491
+
492
+ # Check if the ENTIRE text is just a single databinding expression
493
+ full_match = pattern.fullmatch(text.strip())
494
+ if full_match:
495
+ # Pure expression - return the actual value (not converted to string)
496
+ var_expr = full_match.group(1).strip()
497
+ # EXPR-7: an attribute that is only `{1}` is the number 1. The
498
+ # regex-quantifier exception exists for `\d{10,11}`; a quantifier
499
+ # with nothing before it quantifies nothing, and reading it as text
500
+ # made `<q:return value="{1}"/>` return the string '{1}'.
501
+ if var_expr.isdigit() and text.strip() == full_match.group(0):
502
+ return int(var_expr)
503
+ try:
504
+ return self._evaluate_databinding_expression(var_expr, context)
505
+ except Exception as exc:
506
+ return self._unresolved(var_expr, exc, text)
507
+
508
+ # Mixed content (text + expressions) - need string interpolation
509
+ def replace_variable(match):
510
+ var_expr = match.group(1).strip()
511
+ try:
512
+ result = self._evaluate_databinding_expression(var_expr, context)
513
+ return str(result)
514
+ except Exception as exc:
515
+ return self._unresolved(var_expr, exc, match.group(0))
516
+
517
+ # Replace all {variable} patterns using pre-compiled pattern
518
+ result = pattern.sub(replace_variable, text)
519
+ return result
520
+
521
+ def _unresolved(self, expr: str, exc: BaseException, literal: str) -> Any:
522
+ """An expression in a q: attribute that could not be evaluated.
523
+
524
+ EXPR-1/EXPR-2: an undefined name or a failed evaluation is an error
525
+ that names the expression. It used to be logged once and replaced by
526
+ the literal placeholder — `x{nada}y` came back as 'x{nada}y' and
527
+ `{10 / z}` with z=0 as '{10 / z}', so the failure surfaced, if at all,
528
+ as stray braces on a page or in a database row.
529
+
530
+ Two things are not expressions and keep their text: the inside of a
531
+ JSON object written in a value, and a regex quantifier like {10,11} —
532
+ Quantum has no escape for a literal brace.
533
+ """
534
+ if looks_like_json_object(expr) or is_regex_quantifier(expr):
535
+ report_unresolved(expr, exc)
536
+ return literal
537
+ raise ExpressionError(f"{{{expr}}} could not be evaluated: {exc}") from exc
538
+
539
+ def _evaluate_databinding_expression(self, expr: str, context: Dict[str, Any]) -> Any:
540
+ """Evaluate a databinding expression like 'variable' or 'user.name' or 'functionName(args)' or 'result[0].id'
541
+
542
+ The AST-whitelist evaluator (quantum.core.expressions) handles this now.
543
+ Two things still route around it, and both are deliberate:
544
+
545
+ - `q:function` calls, which need the FunctionRegistry, named-argument
546
+ mapping and nested component resolution the legacy path implements.
547
+ - Scoped variables (session., application., request., form., ...),
548
+ whose contract is to resolve to '' when absent rather than to fail
549
+ (EXPR-3).
550
+
551
+ Anything the evaluator refuses raises, and _apply_databinding turns
552
+ that into an ExpressionError naming the expression (EXPR-1/EXPR-2).
553
+
554
+ There was a fallback chain here during the migration. It was measured
555
+ rather than judged: across pytest's declared testpaths it took 863
556
+ calls over 434 distinct expressions and failed all 863 — not one
557
+ expression it could resolve that the evaluator could not. It was
558
+ deleted rather than kept as reassurance, because two grammars that
559
+ disagree is the problem this phase set out to remove.
560
+
561
+ (The commit that deleted it says "73 calls, 73 failures". That figure
562
+ came from `pytest tests/`, which is a subset of the `testpaths` in
563
+ pytest.ini. The load-bearing half — zero resolved — holds under every
564
+ scope measured; the count does not.)
565
+ """
566
+ # Regex quantifiers are not expressions. See is_regex_quantifier().
567
+ if is_regex_quantifier(expr):
568
+ raise ValueError(f"'{expr}' is a regex quantifier, not an expression")
569
+
570
+ # Isto desviava para _evaluate_function_call qualquer expressao que
571
+ # COMECASSE com uma chamada de q:function — e la o regex
572
+ # `(\w+)\((.*)\)` pega tudo ate o ultimo parentese e ignora o
573
+ # resto. `{dobro(a) * 10}` devolvia 10 em vez de 100: o `* 10` era
574
+ # descartado em silencio. `10 * dobro(a)` funcionava, porque nao
575
+ # comecava com a chamada — a mesma conta dava resultados diferentes
576
+ # conforme a ordem dos fatores.
577
+ #
578
+ # O avaliador ja resolve q:function pelo function_resolver e compoe
579
+ # com aritmetica corretamente, entao o desvio nao e mais necessario.
580
+ # Conferido: dobro(a), dobro(a) * 10, dobro(a) + dobro(a) e
581
+ # 10 * dobro(a) dao 10, 100, 20 e 100.
582
+
583
+ try:
584
+ return self._expressions.evaluate(expr, context)
585
+ except ExpressionError as exc:
586
+ # Scoped variables resolve to '' when absent, matching
587
+ # get_variable()'s default: templates render before login. Shared
588
+ # with HTMLRenderer so both passes agree.
589
+ # EXPR-3: an absent scoped value reads as '' only when the
590
+ # expression IS the reference. Inside an operation it is an error:
591
+ # {session.visitas + 1} used to evaluate to '' on the first visit,
592
+ # and the failure surfaced later as "could not convert string to
593
+ # float: ''" in a q:set with type="number".
594
+ if is_absent_scope(expr):
595
+ if _PLAIN_REFERENCE.match(expr):
596
+ return ''
597
+ raise ExpressionError(
598
+ f"{root_scope(expr)} value used in {expr!r} is not set. "
599
+ f"Check it with q:if first, or for a counter use "
600
+ f"<q:set operation=\"increment\">.") from exc
601
+ raise ValueError(str(exc)) from exc
602
+
603
+ def _evaluate_function_call(self, expr: str, context: Dict[str, Any]) -> Any:
604
+ """
605
+ Evaluate function call expression like 'add(5, 3)' or 'calculateTotal(items, 0.1)'
606
+ """
607
+ # Parse function name and arguments
608
+ match = re.match(r'(\w+)\((.*)\)', expr.strip())
609
+ if not match:
610
+ raise ComponentExecutionError(f"Invalid function call syntax: {expr}")
611
+
612
+ func_name = match.group(1)
613
+ args_str = match.group(2).strip()
614
+
615
+ # Resolve function
616
+ func_node = self.function_registry.resolve_function(func_name, self.current_component)
617
+ if not func_node:
618
+ raise ComponentExecutionError(f"Function '{func_name}' not found")
619
+
620
+ # Parse arguments
621
+ args = self._parse_function_arguments(args_str, context, func_node)
622
+
623
+ # Execute function
624
+ return self._execute_function(func_node, args)
625
+
626
+ def _parse_function_arguments(self, args_str: str, context: Dict[str, Any], func_node: FunctionNode) -> Dict[str, Any]:
627
+ """Parse function arguments from string like '5, 3' or 'items, 0.1' or 'add(2, 3), 4'"""
628
+ if not args_str:
629
+ return {}
630
+
631
+ # Smart split by comma - respect nested parentheses
632
+ args = self._smart_split_args(args_str)
633
+
634
+ arg_values = []
635
+ for arg in args:
636
+ arg = arg.strip()
637
+
638
+ # Evaluate each argument as an expression
639
+ try:
640
+ # Try to evaluate as databinding expression (handles nested function calls)
641
+ value = self._evaluate_databinding_expression(arg, context)
642
+ arg_values.append(value)
643
+ except:
644
+ # Fallback: try as literal
645
+ try:
646
+ # Try to parse as number
647
+ if '.' in arg:
648
+ arg_values.append(float(arg))
649
+ else:
650
+ arg_values.append(int(arg))
651
+ except:
652
+ # Use as string (remove quotes if present)
653
+ if arg.startswith('\"') and arg.endswith('\"'):
654
+ arg_values.append(arg[1:-1])
655
+ elif arg.startswith("'") and arg.endswith("'"):
656
+ arg_values.append(arg[1:-1])
657
+ else:
658
+ arg_values.append(arg)
659
+
660
+ # Match positional args to parameter names
661
+ return dict(zip([p.name for p in func_node.params], arg_values))
662
+
663
+ def _smart_split_args(self, args_str: str) -> List[str]:
664
+ """Split arguments by comma, respecting nested parentheses"""
665
+ args = []
666
+ current_arg = []
667
+ paren_depth = 0
668
+
669
+ for char in args_str:
670
+ if char == '(':
671
+ paren_depth += 1
672
+ current_arg.append(char)
673
+ elif char == ')':
674
+ paren_depth -= 1
675
+ current_arg.append(char)
676
+ elif char == ',' and paren_depth == 0:
677
+ # Top-level comma - split here
678
+ args.append(''.join(current_arg))
679
+ current_arg = []
680
+ else:
681
+ current_arg.append(char)
682
+
683
+ # Add last argument
684
+ if current_arg:
685
+ args.append(''.join(current_arg))
686
+
687
+ return args
688
+
689
+ # -- the interface InvocationService expects -------------------------
690
+ #
691
+ # q:invoke function="..." routed through InvocationService, which calls
692
+ # context.get_function(name) and context.execute_function(name, args) on
693
+ # whatever it is handed — and it is handed this runtime, which had
694
+ # neither method. So the service returned
695
+ # "Context does not support function invocation" and q:invoke silently
696
+ # produced nothing. The tenth instance of this project's recurring
697
+ # pattern: a service written against an imagined interface, with the
698
+ # tests mocking that same imagination.
699
+ #
700
+ # Implemented against the real FunctionRegistry rather than rewriting
701
+ # the service, because the interface it assumed is a reasonable one.
702
+
703
+ def get_function(self, name: str):
704
+ """The FunctionNode for `name`, or None."""
705
+ return self.function_registry.resolve_function(name, self.current_component)
706
+
707
+ def _resolve_expression_function(self, name: str):
708
+ """Wrap a q:function so an expression can call it positionally.
709
+
710
+ `{somaTotal(itens)}` passes arguments by position; q:function binds
711
+ them by parameter name, so they are zipped against the declared
712
+ params — the same rule q:invoke uses.
713
+ """
714
+ func_node = self.get_function(name)
715
+ if not func_node:
716
+ return None
717
+
718
+ def call(*args, **kwargs):
719
+ bound = dict(zip([p.name for p in func_node.params], args))
720
+ bound.update(kwargs)
721
+ return self._execute_function(func_node, bound)
722
+
723
+ call.__name__ = name
724
+ return call
725
+
726
+ def execute_function(self, name: str, args: Dict[str, Any]) -> Dict[str, Any]:
727
+ """Execute a q:function by name. Returns the service's result shape."""
728
+ func_node = self.get_function(name)
729
+ if not func_node:
730
+ return {'success': False, 'error': {'message': f"Function '{name}' not found"}}
731
+ try:
732
+ return {'success': True, 'data': self._execute_function(func_node, args or {})}
733
+ except Exception as exc: # noqa: BLE001
734
+ # INV-2: the failure is reported in <name>_result, with the same
735
+ # {message} shape as an HTTP failure. This used to be
736
+ # logger.exception, which printed a Python traceback to the person
737
+ # running `quantum run` for what is an ordinary, handled failure.
738
+ logger.warning("q:invoke function=%r failed: %s", name, exc)
739
+ logger.debug("q:invoke function=%r traceback", name, exc_info=True)
740
+ return {'success': False, 'error': {'message': str(exc)}}
741
+
742
+ def _execute_function(self, func_node: FunctionNode, args: Dict[str, Any]) -> Any:
743
+ """Execute function with given arguments"""
744
+
745
+ # Create function execution context (child of current context)
746
+ func_context = self.execution_context.create_child_context()
747
+
748
+ # Bind parameters to context
749
+ for param in func_node.params:
750
+ value = args.get(param.name)
751
+
752
+ # Use default if not provided
753
+ if value is None and param.default is not None:
754
+ value = param.default
755
+
756
+ # Check required
757
+ if param.required and value is None:
758
+ raise ComponentExecutionError(f"Required parameter '{param.name}' not provided")
759
+
760
+ # FN-1: the same q:param as q:action and the component — converted
761
+ # to its type and checked against its rules, on every call. This
762
+ # used to run only with validate="true", and even then checked
763
+ # neither type="email" nor min/max: f(150) passed max="100".
764
+ if value is not None:
765
+ value, erro = param_validation.coerce(param, value)
766
+ erros = [erro] if erro else param_validation.check_rules(param, value)
767
+ if erros:
768
+ raise ComponentExecutionError('; '.join(erros))
769
+ self._validate_function_args(func_node, {param.name: value}, only=param)
770
+
771
+ # Set in function context
772
+ func_context.set_variable(param.name, value, scope="local")
773
+
774
+ # Execute function body using modular executors.
775
+ #
776
+ # The context is swapped, not just passed. BaseExecutor.context is
777
+ # `self._runtime.execution_context` and get_all_variables() reads it,
778
+ # so every executor in the body looked at the COMPONENT's variables
779
+ # and never at the function's — a q:function could not see its own
780
+ # parameters. `<q:loop items="{numbers}">` inside a function whose
781
+ # parameter is `numbers` failed with "variable 'numbers' is not
782
+ # defined, did you mean 'myNumbers'?", naming the caller's variable.
783
+ previous_context = self.execution_context
784
+ self.execution_context = func_context
785
+ try:
786
+ result = None
787
+ for statement in func_node.body:
788
+ if isinstance(statement, QuantumReturn):
789
+ # Evaluate return value
790
+ result = self._process_return_value(
791
+ statement.value, func_context.get_all_variables())
792
+ break
793
+ else:
794
+ # Delegate all other statements to the executor registry
795
+ result = self._execute_statement(statement, func_context)
796
+ if produced_return(statement, result):
797
+ break
798
+ finally:
799
+ self.execution_context = previous_context
800
+
801
+ return self._check_return_type(func_node, as_return_value(result))
802
+
803
+ def _check_return_type(self, func_node: FunctionNode, valor: Any) -> Any:
804
+ """FN-4: the value a q:function returns matches its returnType.
805
+
806
+ returnType= was accepted and never read: returnType="number" returned
807
+ whatever the body produced.
808
+ """
809
+ tipo = (getattr(func_node, 'return_type', None) or 'any').lower()
810
+ if tipo == 'any':
811
+ return valor
812
+ if tipo == 'void':
813
+ if valor is not None:
814
+ raise ComponentExecutionError(
815
+ f'q:function "{func_node.name}" declares returnType="void" but returned {valor!r}')
816
+ return valor
817
+ if valor is None:
818
+ raise ComponentExecutionError(
819
+ f'q:function "{func_node.name}" declares returnType="{tipo}" but returned nothing')
820
+ from types import SimpleNamespace
821
+ convertido, erro = param_validation.coerce(SimpleNamespace(type=tipo, name=func_node.name), valor)
822
+ if erro:
823
+ raise ComponentExecutionError(
824
+ f'q:function "{func_node.name}" declares returnType="{tipo}" but returned {valor!r}')
825
+ return convertido
826
+
827
+ def _validate_function_args(self, func_node: FunctionNode, args: Dict[str, Any], only=None):
828
+ """validate=/range=/enum= rules of the q:param (QuantumValidators)."""
829
+ for param in ([only] if only is not None else func_node.params):
830
+ value = args.get(param.name)
831
+
832
+ # Check required
833
+ if param.required and value is None:
834
+ raise ComponentExecutionError(f"Required parameter '{param.name}' is missing")
835
+
836
+ # Skip validation if no value provided
837
+ if value is None:
838
+ continue
839
+
840
+ # Validate using QuantumValidators
841
+ if param.validate_rule:
842
+ # Use the generic validate method for all rules
843
+ is_valid, error = QuantumValidators.validate(value, param.validate_rule)
844
+ if not is_valid:
845
+ raise ValidationError(f"Parameter '{param.name}': {error}")
846
+
847
+ # Validate range
848
+ if param.range:
849
+ is_valid, error = QuantumValidators.validate_range(value, param.range)
850
+ if not is_valid:
851
+ raise ValidationError(f"Parameter '{param.name}': {error}")
852
+
853
+ # Validate enum
854
+ if param.enum:
855
+ is_valid, error = QuantumValidators.validate_enum(value, param.enum)
856
+ if not is_valid:
857
+ raise ValidationError(f"Parameter '{param.name}': {error}")
858
+
859
+ def _execute_dispatch_event(self, dispatch_node: DispatchEventNode, context: Dict[str, Any]):
860
+ """Execute q:dispatchEvent statement"""
861
+ # For now, just log the event (TODO: integrate with actual message queue)
862
+ event_data = {
863
+ 'event': dispatch_node.event,
864
+ 'data': self._apply_databinding(dispatch_node.data, context) if dispatch_node.data else None,
865
+ 'queue': dispatch_node.queue,
866
+ 'priority': dispatch_node.priority,
867
+ }
868
+
869
+ print(f"[EVENT DISPATCHED] {event_data}")
870
+ # TODO: Integrate with RabbitMQ/SQS/etc
871
+
872
+ def _execute_on_event(self, event_node: OnEventNode, event_data: Dict[str, Any]):
873
+ """Execute q:onEvent handler (called by event system)"""
874
+ # Create event context
875
+ event_context = self.execution_context.create_child_context(scope="local")
876
+
877
+ # Add event data to context
878
+ for key, value in event_data.items():
879
+ event_context.set_variable(key, value, scope="local")
880
+
881
+ # Execute event handler body using modular executors
882
+ for statement in event_node.body:
883
+ self._execute_statement(statement, event_context)
884
+
885
+ def _execute_knowledge_query(self, query_node: QueryNode, resolved_params: dict, exec_context: ExecutionContext):
886
+ """
887
+ Execute a q:query against a knowledge: virtual datasource.
888
+
889
+ Supports two modes:
890
+ - Search mode (default): vector similarity search, returns [{content, relevance, source}]
891
+ - RAG mode (mode="rag"): search + LLM answer, returns {answer, sources, confidence}
892
+
893
+ Args:
894
+ query_node: QueryNode with datasource="knowledge:{name}"
895
+ resolved_params: Resolved query parameters
896
+ exec_context: Execution context
897
+ """
898
+ from quantum.runtime.database_service import QueryResult
899
+
900
+ try:
901
+ # Extract knowledge base name from datasource
902
+ kb_name = query_node.datasource.replace('knowledge:', '', 1)
903
+
904
+ # Check if knowledge base failed to initialize - return empty results
905
+ kb_meta = exec_context.get_variable(f"_knowledge_{kb_name}")
906
+ if isinstance(kb_meta, dict) and kb_meta.get("_failed"):
907
+ from quantum.runtime.database_service import QueryResult as _QR
908
+ mode = query_node.mode
909
+ if mode == 'rag':
910
+ empty_data = [{"answer": "Knowledge base is still loading. Please refresh the page.", "sources": "", "confidence": "0"}]
911
+ result = _QR(data=empty_data, column_list=['answer', 'sources', 'confidence'], execution_time=0.0, record_count=1, success=True)
912
+ else:
913
+ result = _QR(data=[], column_list=['content', 'relevance', 'source', 'chunk_index'], execution_time=0.0, record_count=0, success=True)
914
+ result_dict = result.to_dict()
915
+ exec_context.set_variable(query_node.name, result.data, scope="component")
916
+ exec_context.set_variable(f"{query_node.name}_result", result_dict, scope="component")
917
+ self.context[query_node.name] = result.data
918
+ self.context[f"{query_node.name}_result"] = result_dict
919
+ if result.data and len(result.data) == 1 and isinstance(result.data[0], dict):
920
+ for field_name, field_value in result.data[0].items():
921
+ dotted_key = f"{query_node.name}.{field_name}"
922
+ exec_context.set_variable(dotted_key, field_value, scope="component")
923
+ self.context[dotted_key] = field_value
924
+ return result
925
+
926
+ # Get the search query from the first parameter value
927
+ search_text = None
928
+ for param_name, param_value in resolved_params.items():
929
+ search_text = str(param_value)
930
+ break
931
+
932
+ if not search_text:
933
+ raise KnowledgeError("Knowledge query requires at least one parameter for the search query")
934
+
935
+ # Parse LIMIT from SQL if present
936
+ n_results = 5
937
+ limit_match = re.search(r'LIMIT\s+(\d+)', query_node.sql, re.IGNORECASE)
938
+ if limit_match:
939
+ n_results = int(limit_match.group(1))
940
+
941
+ # Get embed model from knowledge base metadata
942
+ embed_model = "nomic-embed-text"
943
+ kb_model = None
944
+ if isinstance(kb_meta, dict):
945
+ embed_model = kb_meta.get("embed_model", embed_model)
946
+ kb_model = kb_meta.get("model")
947
+
948
+ # Determine mode
949
+ mode = query_node.mode # None or "rag"
950
+
951
+ if mode == 'rag':
952
+ # RAG mode: search + LLM answer
953
+ rag_model = query_node.rag_model or kb_model
954
+ result_data = self.knowledge_service.rag_query(
955
+ name=kb_name,
956
+ question=search_text,
957
+ model=rag_model,
958
+ n_results=n_results,
959
+ embed_model=embed_model,
960
+ )
961
+
962
+ # Store as single-row result for {name.field} access
963
+ data = [result_data]
964
+ result = QueryResult(
965
+ data=data,
966
+ column_list=['answer', 'sources', 'confidence'],
967
+ execution_time=0.0,
968
+ record_count=1,
969
+ success=True,
970
+ )
971
+
972
+ else:
973
+ # Search mode: vector similarity
974
+ search_results = self.knowledge_service.search(
975
+ name=kb_name,
976
+ query_text=search_text,
977
+ n_results=n_results,
978
+ embed_model=embed_model,
979
+ )
980
+
981
+ data = search_results
982
+ result = QueryResult(
983
+ data=data,
984
+ column_list=['content', 'relevance', 'source', 'chunk_index'],
985
+ execution_time=0.0,
986
+ record_count=len(data),
987
+ success=True,
988
+ )
989
+
990
+ # Store results in context (same pattern as regular query)
991
+ result_dict = result.to_dict()
992
+ exec_context.set_variable(query_node.name, result.data, scope="component")
993
+ exec_context.set_variable(f"{query_node.name}_result", result_dict, scope="component")
994
+ self.context[query_node.name] = result.data
995
+ self.context[f"{query_node.name}_result"] = result_dict
996
+
997
+ # For single-row results, expose fields directly ({name.field})
998
+ if result.data and len(result.data) == 1 and isinstance(result.data[0], dict):
999
+ for field_name, field_value in result.data[0].items():
1000
+ dotted_key = f"{query_node.name}.{field_name}"
1001
+ exec_context.set_variable(dotted_key, field_value, scope="component")
1002
+ self.context[dotted_key] = field_value
1003
+
1004
+ if query_node.result:
1005
+ exec_context.set_variable(query_node.result, result_dict, scope="component")
1006
+ self.context[query_node.result] = result_dict
1007
+
1008
+ return result
1009
+
1010
+ except KnowledgeError as e:
1011
+ raise ComponentExecutionError(f"Knowledge query error in '{query_node.name}': {e}")
1012
+ except Exception as e:
1013
+ raise ComponentExecutionError(f"Knowledge query execution error in '{query_node.name}': {e}")
1014
+
1015
+ # ============================================
1016
+ # JOB EXECUTION SYSTEM (q:schedule, q:thread, q:job)
1017
+
1018
+
1019
+ def _delegate_to_container(attribute: str, service: str) -> property:
1020
+ """runtime.<attribute> is services.<service>: one lazy instance per runtime.
1021
+
1022
+ Assigning still works (tests replace a service with a double), and the
1023
+ replacement is what every executor sees, through either name.
1024
+ """
1025
+ def get(self):
1026
+ return getattr(self._services, service)
1027
+
1028
+ def set_(self, value):
1029
+ self._services._services[service] = value
1030
+
1031
+ return property(get, set_, doc=f"Same instance as services.{service}.")
1032
+
1033
+
1034
+ for _attribute, _service in (
1035
+ ('database_service', 'database'),
1036
+ ('invocation_service', 'invocation'),
1037
+ ('data_import_service', 'data_import'),
1038
+ ('logging_service', 'logging'),
1039
+ ('dump_service', 'dump'),
1040
+ ('file_upload_service', 'file_upload'),
1041
+ ('email_service', 'email'),
1042
+ ('llm_service', 'llm'),
1043
+ ('knowledge_service', 'knowledge'),
1044
+ ('message_queue_service', 'message_queue'),
1045
+ ('job_executor', 'job_executor'),
1046
+ ):
1047
+ setattr(ComponentRuntime, _attribute, _delegate_to_container(_attribute, _service))
1048
+ del _attribute, _service