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