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