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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- 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)
|