quantum-framework 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,217 @@
1
+ """
2
+ Agent Executor - Execute q:agent statements
3
+
4
+ Handles AI agent execution with tool use capabilities.
5
+ """
6
+
7
+ from typing import Any, List, Dict, Type
8
+ from quantum.runtime.executors.base import BaseExecutor, ExecutorError
9
+ from quantum.core.ast_nodes import (
10
+ QuantumReturn,
11
+ HTMLNode, TextNode, DocTypeNode, CommentNode,
12
+ )
13
+
14
+ # Import from features module
15
+ try:
16
+ from quantum.core.features.agents.src.ast_node import AgentNode
17
+ except ImportError:
18
+ from quantum.core.features.agents.src import AgentNode
19
+
20
+ _RENDER_ONLY_NODE_TYPES = (HTMLNode, TextNode, DocTypeNode, CommentNode)
21
+
22
+
23
+ def build_tool_definitions(tool_nodes) -> List[Dict[str, Any]]:
24
+ """
25
+ Build the tool definitions AgentService actually consumes.
26
+
27
+ AgentService._build_tools_description() reads name/description/params
28
+ (a list of param dicts), and AgentService._execute_tool() reads
29
+ tool_def["body"] to hand the AST statements to tool_executor. The
30
+ JSON-Schema shape from AgentToolNode.get_schema() has neither, which is
31
+ why tools used to silently degrade to the "no handler" placeholder.
32
+ """
33
+ from quantum.runtime.agent_service import BUILTIN_TOOLS
34
+
35
+ tools = []
36
+ for tool in tool_nodes:
37
+ if tool.builtin:
38
+ builtin_def = BUILTIN_TOOLS.get(tool.name)
39
+ if builtin_def:
40
+ tools.append(dict(builtin_def))
41
+ continue
42
+
43
+ tools.append({
44
+ 'name': tool.name,
45
+ 'description': tool.description,
46
+ 'params': [p.to_dict() for p in tool.params],
47
+ 'body': tool.body,
48
+ })
49
+ return tools
50
+
51
+
52
+ class AgentExecutor(BaseExecutor):
53
+ """
54
+ Executor for q:agent statements.
55
+
56
+ Supports:
57
+ - Multi-turn agent reasoning loop
58
+ - Tool calling and execution
59
+ - Multiple LLM providers (Ollama, OpenAI, Anthropic)
60
+ - Iteration limits and timeouts
61
+ """
62
+
63
+ @property
64
+ def handles(self) -> List[Type]:
65
+ return [AgentNode]
66
+
67
+ def execute(self, node: AgentNode, exec_context) -> Any:
68
+ """
69
+ Execute AI agent.
70
+
71
+ Args:
72
+ node: AgentNode with agent configuration
73
+ exec_context: Execution context
74
+
75
+ Returns:
76
+ Agent response dict with result and actions
77
+ """
78
+ try:
79
+ context = exec_context.get_all_variables()
80
+
81
+ # Resolve task from execute node
82
+ if not node.execute:
83
+ raise ExecutorError("Agent requires <q:execute> to run")
84
+
85
+ task = self.apply_databinding(node.execute.task, context)
86
+ task_context = ""
87
+ if node.execute.context:
88
+ task_context = self.apply_databinding(node.execute.context, context)
89
+
90
+ # Build instruction
91
+ instruction = ""
92
+ if node.instruction:
93
+ instruction = self.apply_databinding(node.instruction.content, context)
94
+
95
+ tools = build_tool_definitions(node.tools)
96
+
97
+ api_key = ""
98
+ if node.api_key:
99
+ api_key = self.apply_databinding(node.api_key, context)
100
+
101
+ endpoint = ""
102
+ if node.endpoint:
103
+ endpoint = self.apply_databinding(node.endpoint, context)
104
+
105
+ agent_result = self.services.agent.execute(
106
+ instruction=instruction,
107
+ tools=tools,
108
+ task=task,
109
+ context=task_context,
110
+ model=node.model,
111
+ endpoint=endpoint,
112
+ provider=node.provider,
113
+ api_key=api_key,
114
+ max_iterations=node.max_iterations,
115
+ timeout_ms=node.timeout,
116
+ tool_executor=build_tool_executor(self, exec_context, node.tools),
117
+ )
118
+
119
+ result = agent_result.to_dict()
120
+
121
+ # Store results
122
+ exec_context.set_variable(node.name, result.get('result', ''), scope="component")
123
+ exec_context.set_variable(f"{node.name}_result", result, scope="component")
124
+
125
+ return result
126
+
127
+ except Exception as e:
128
+ error_result = {
129
+ 'success': False,
130
+ 'error': {'message': str(e)},
131
+ 'result': '',
132
+ 'iterations': 0,
133
+ 'actions': []
134
+ }
135
+ exec_context.set_variable(f"{node.name}_result", error_result, scope="component")
136
+ raise ExecutorError(f"Agent execution error: {e}")
137
+
138
+
139
+ def coerce_tool_args(params, tool_args: Dict[str, Any]) -> Dict[str, Any]:
140
+ """
141
+ Coerce LLM-supplied tool arguments to their declared q:param types.
142
+
143
+ An LLM sends every argument as a string ({"a": "17"}), so a tool body
144
+ doing {a + b} would concatenate instead of adding. The declared
145
+ <q:param type="number"> is what tells us the intent.
146
+ """
147
+ by_name = {p.name: p for p in (params or [])}
148
+ coerced = {}
149
+
150
+ for key, value in (tool_args or {}).items():
151
+ param = by_name.get(key)
152
+ if param is None or not isinstance(value, str):
153
+ coerced[key] = value
154
+ continue
155
+
156
+ try:
157
+ if param.type in ('number', 'decimal'):
158
+ coerced[key] = float(value) if '.' in value else int(value)
159
+ elif param.type == 'integer':
160
+ coerced[key] = int(value)
161
+ elif param.type == 'boolean':
162
+ coerced[key] = value.strip().lower() in ('true', '1', 'yes', 'on')
163
+ elif param.type in ('array', 'object'):
164
+ import json
165
+ coerced[key] = json.loads(value)
166
+ else:
167
+ coerced[key] = value
168
+ except (ValueError, TypeError):
169
+ # Leave the raw value in place — the tool body may still handle it,
170
+ # and a hard failure here would be worse than a best-effort pass.
171
+ coerced[key] = value
172
+
173
+ return coerced
174
+
175
+
176
+ def build_tool_executor(executor: BaseExecutor, exec_context, tool_nodes=None):
177
+ """
178
+ Build the tool_executor callable AgentService/AgentTeam expect:
179
+ tool_executor(tool_name, tool_args, body) -> Any
180
+
181
+ Runs the tool's AST body in a child context with the LLM-supplied
182
+ arguments coerced to their declared types and bound as local variables.
183
+ """
184
+ params_by_tool = {
185
+ tool.name: tool.params for tool in (tool_nodes or []) if not tool.builtin
186
+ }
187
+
188
+ def tool_executor(tool_name: str, tool_args: Dict[str, Any], body: List[Any]) -> Any:
189
+ child_context = exec_context.create_child_context()
190
+ args = coerce_tool_args(params_by_tool.get(tool_name), tool_args)
191
+ for key, value in args.items():
192
+ child_context.set_variable(key, value, scope="local")
193
+ return _run_tool_body(executor, body, child_context)
194
+
195
+ return tool_executor
196
+
197
+
198
+ def _run_tool_body(executor: BaseExecutor, body: List[Any], child_context) -> Any:
199
+ """Execute a tool body, returning the first q:return value found."""
200
+ for statement in body or []:
201
+ if isinstance(statement, QuantumReturn):
202
+ return executor.resolve_value(statement.value, child_context.get_all_variables())
203
+
204
+ if isinstance(statement, _RENDER_ONLY_NODE_TYPES):
205
+ continue
206
+
207
+ # <q:tool> commonly wraps its implementation in a <q:function> —
208
+ # descend into it rather than registering it as a callable function.
209
+ if type(statement).__name__ == 'FunctionNode':
210
+ result = _run_tool_body(executor, getattr(statement, 'body', []), child_context)
211
+ if result is not None:
212
+ return result
213
+ continue
214
+
215
+ executor.runtime.executor_registry.execute(statement, child_context)
216
+
217
+ return None
@@ -0,0 +1,114 @@
1
+ """
2
+ Knowledge Executor - Execute q:knowledge statements
3
+
4
+ Handles knowledge base creation and RAG operations.
5
+ """
6
+
7
+ import copy
8
+ from typing import Any, List, Dict, Type
9
+ from quantum.runtime.executors.base import BaseExecutor, ExecutorError
10
+
11
+ # Import from features module
12
+ try:
13
+ from quantum.core.features.knowledge_base.src.ast_node import KnowledgeNode
14
+ except ImportError:
15
+ from quantum.core.features.knowledge_base.src import KnowledgeNode
16
+
17
+
18
+ class KnowledgeExecutor(BaseExecutor):
19
+ """
20
+ Executor for q:knowledge statements.
21
+
22
+ Supports:
23
+ - Multiple source types (text, file, directory, URL, query)
24
+ - Embedding generation
25
+ - Vector storage (ChromaDB)
26
+ - RAG queries via q:query knowledge="name"
27
+ """
28
+
29
+ @property
30
+ def handles(self) -> List[Type]:
31
+ return [KnowledgeNode]
32
+
33
+ def execute(self, node: KnowledgeNode, exec_context) -> Any:
34
+ """
35
+ Execute knowledge base creation.
36
+
37
+ Args:
38
+ node: KnowledgeNode with knowledge base configuration
39
+ exec_context: Execution context
40
+
41
+ Returns:
42
+ Knowledge base info dict
43
+ """
44
+ try:
45
+ context = exec_context.get_all_variables()
46
+
47
+ # KnowledgeService.index_knowledge() expects KnowledgeSourceNode
48
+ # objects (attribute access: source.content, source.path, ...),
49
+ # not dicts — build resolved copies rather than a dict config.
50
+ sources = []
51
+ for source in node.sources:
52
+ resolved = copy.copy(source)
53
+
54
+ if source.source_type == 'text' and source.content:
55
+ resolved.content = self.apply_databinding(source.content, context)
56
+ elif source.source_type in ('file', 'directory') and source.path:
57
+ resolved.path = self.apply_databinding(source.path, context)
58
+ elif source.source_type == 'url' and source.url:
59
+ resolved.url = self.apply_databinding(source.url, context)
60
+ elif source.source_type == 'query' and source.sql:
61
+ resolved.sql = self.apply_databinding(source.sql, context)
62
+
63
+ sources.append(resolved)
64
+
65
+ # Index into the SAME KnowledgeService instance that
66
+ # q:query datasource="knowledge:..." searches later
67
+ # (runtime.knowledge_service — see component.py's
68
+ # _execute_knowledge_query). services.knowledge is a separate
69
+ # instance and indexing into it would leave searches empty.
70
+ knowledge_service = self.runtime.knowledge_service
71
+ knowledge_service.index_knowledge(
72
+ name=node.name,
73
+ sources=sources,
74
+ embed_model=node.embed_model,
75
+ chunk_size=node.chunk_size,
76
+ chunk_overlap=node.chunk_overlap,
77
+ persist=node.persist,
78
+ persist_path=node.persist_path,
79
+ rebuild=node.rebuild,
80
+ database_service=self.services.database,
81
+ exec_context=exec_context,
82
+ )
83
+
84
+ info = {
85
+ 'success': True,
86
+ 'name': node.name,
87
+ 'model': node.model,
88
+ 'embed_model': node.embed_model,
89
+ }
90
+
91
+ # Store knowledge base reference (name resolves via q:query
92
+ # datasource="knowledge:{name}", not by reading this variable)
93
+ exec_context.set_variable(node.name, info, scope="component")
94
+ exec_context.set_variable(f"{node.name}_info", info, scope="component")
95
+ exec_context.set_variable(f"_knowledge_{node.name}", {
96
+ 'embed_model': node.embed_model,
97
+ 'model': node.model,
98
+ }, scope="component")
99
+
100
+ return info
101
+
102
+ except Exception as e:
103
+ exec_context.set_variable(f"{node.name}_info", {
104
+ 'success': False,
105
+ 'error': str(e),
106
+ 'name': node.name
107
+ }, scope="component")
108
+ # _execute_knowledge_query() (component.py) checks this marker to
109
+ # return a graceful "still loading" result instead of raising
110
+ # KnowledgeError("not found") on the next q:query for this KB.
111
+ exec_context.set_variable(f"_knowledge_{node.name}", {
112
+ '_failed': True,
113
+ }, scope="component")
114
+ raise ExecutorError(f"Knowledge base creation error: {e}")
@@ -0,0 +1,153 @@
1
+ """
2
+ LLM Executor - Execute q:llm statements
3
+
4
+ Handles LLM invocation across providers (Ollama, OpenAI, Anthropic, LM Studio).
5
+ """
6
+
7
+ import json
8
+ from typing import Any, List, Dict, Type
9
+ from quantum.runtime.executors.base import BaseExecutor, ExecutorError
10
+ from quantum.core.ast_nodes import LLMNode
11
+
12
+
13
+ class LLMExecutor(BaseExecutor):
14
+ """
15
+ Executor for q:llm statements.
16
+
17
+ Supports:
18
+ - Completion mode (single prompt) and chat mode (messages array)
19
+ - Multiple providers via provider= / endpoint= / apiKey=
20
+ - JSON response format
21
+ - Temperature and token limits
22
+ - Response caching with TTL
23
+ """
24
+
25
+ @property
26
+ def handles(self) -> List[Type]:
27
+ return [LLMNode]
28
+
29
+ def execute(self, node: LLMNode, exec_context) -> Any:
30
+ """
31
+ Execute LLM invocation.
32
+
33
+ Args:
34
+ node: LLMNode with LLM configuration
35
+ exec_context: Execution context
36
+
37
+ Returns:
38
+ LLM response (text or parsed JSON)
39
+ """
40
+ try:
41
+ context = exec_context.get_all_variables()
42
+
43
+ model = node.model or 'phi3'
44
+ provider = node.provider or None
45
+ endpoint = self.apply_databinding(node.endpoint, context) if node.endpoint else None
46
+ api_key = self.apply_databinding(node.api_key, context) if node.api_key else None
47
+
48
+ system_text = None
49
+ if node.system:
50
+ system_text = self.apply_databinding(node.system, context)
51
+
52
+ # Chat mode - build the messages array. node.system prepends a
53
+ # system-role message (in addition to any explicit
54
+ # <q:message role="system"> already in node.messages).
55
+ messages = []
56
+ if node.messages:
57
+ if system_text:
58
+ messages.append({'role': 'system', 'content': system_text})
59
+ for msg in node.messages:
60
+ content = self.apply_databinding(msg.content, context)
61
+ messages.append({'role': msg.role, 'content': content})
62
+
63
+ prompt_text = None
64
+ if not messages:
65
+ prompt_text = self.apply_databinding(node.prompt, context) if node.prompt else ''
66
+
67
+ temperature = float(node.temperature) if node.temperature is not None else None
68
+ max_tokens = int(node.max_tokens) if node.max_tokens is not None else None
69
+ response_format = node.response_format or 'text'
70
+
71
+ cache = self.services.llm_cache if node.cache else None
72
+ cache_key = None
73
+ if cache is not None:
74
+ cache_key = cache.build_key(
75
+ provider=provider, model=model, endpoint=endpoint,
76
+ messages=messages, prompt=prompt_text, system=system_text,
77
+ temperature=temperature, max_tokens=max_tokens,
78
+ response_format=response_format,
79
+ )
80
+ cached = cache.get(cache_key)
81
+ if cached is not None:
82
+ return self._store(node, cached, exec_context, cached=True)
83
+
84
+ # MultiProviderLLMService is what q:agent has always used; q:llm
85
+ # was pinned to the Ollama-only LLMService, which is why
86
+ # "multi-provider" was only half true.
87
+ llm = self.services.multi_llm
88
+
89
+ if messages:
90
+ response = llm.chat(
91
+ messages=messages,
92
+ model=model,
93
+ provider=provider,
94
+ endpoint=endpoint,
95
+ api_key=api_key,
96
+ temperature=temperature,
97
+ max_tokens=max_tokens,
98
+ response_format=response_format,
99
+ )
100
+ else:
101
+ response = llm.generate(
102
+ prompt=prompt_text,
103
+ model=model,
104
+ system=system_text,
105
+ provider=provider,
106
+ endpoint=endpoint,
107
+ api_key=api_key,
108
+ temperature=temperature,
109
+ max_tokens=max_tokens,
110
+ response_format=response_format,
111
+ )
112
+
113
+ if cache is not None and response.get('success', True):
114
+ cache.set(cache_key, response, ttl=node.ttl)
115
+
116
+ return self._store(node, response, exec_context, cached=False)
117
+
118
+ except Exception as e:
119
+ exec_context.set_variable(f"{node.name}_result", {
120
+ 'success': False,
121
+ 'error': str(e),
122
+ 'model': node.model
123
+ }, scope="component")
124
+ raise ExecutorError(f"LLM execution error: {e}")
125
+
126
+ def _store(self, node: LLMNode, response: Dict[str, Any], exec_context, cached: bool) -> Any:
127
+ """Unpack a provider response, store it, and return the value."""
128
+ text = response.get('data', '')
129
+ value = text
130
+
131
+ # The provider layer returns raw text; JSON parsing is this executor's
132
+ # job (the single-provider service used to do it, the shared one does not).
133
+ if (node.response_format or 'text') == 'json':
134
+ parsed = response.get('parsed')
135
+ if parsed is None and isinstance(text, str) and text.strip():
136
+ try:
137
+ parsed = json.loads(text)
138
+ except json.JSONDecodeError:
139
+ parsed = None
140
+ if parsed is not None:
141
+ value = parsed
142
+
143
+ exec_context.set_variable(node.name, value, scope="component")
144
+ exec_context.set_variable(f"{node.name}_result", {
145
+ 'success': response.get('success', True),
146
+ 'response': value,
147
+ 'model': response.get('model', node.model),
148
+ 'provider': response.get('provider'),
149
+ 'usage': response.get('usage'),
150
+ 'cached': cached,
151
+ }, scope="component")
152
+
153
+ return value
@@ -0,0 +1,171 @@
1
+ """
2
+ Team Executor - Execute q:team statements
3
+
4
+ Handles multi-agent team execution with handoffs.
5
+ """
6
+
7
+ from typing import Any, List, Dict, Type
8
+ from quantum.runtime.executors.base import BaseExecutor, ExecutorError
9
+ from quantum.runtime.executors.ai.agent_executor import build_tool_definitions, build_tool_executor
10
+
11
+ # Import from features module
12
+ try:
13
+ from quantum.core.features.agents.src.ast_node import AgentTeamNode
14
+ except ImportError:
15
+ from quantum.core.features.agents.src import AgentTeamNode
16
+
17
+
18
+
19
+ # The tools that make a team a team. An agent inside <q:team> gets them
20
+ # automatically.
21
+ #
22
+ # They used to require an explicit <q:tool name="handoff" builtin="true" /> on
23
+ # every agent, and nobody wrote it — examples/multi_agent_support.q advertises
24
+ # "Built-in handoff, readShared, writeShared, listAgents tools" in its header
25
+ # and instructs the router to "use the handoff tool", while declaring none of
26
+ # them. So the tool did not exist, the model could not call it, and the team
27
+ # ended at its first agent reporting success. A q:team exists in order to hand
28
+ # off; making that opt-in was boilerplate everyone forgets.
29
+ #
30
+ # An agent that declares a tool of the same name wins, so this cannot shadow a
31
+ # deliberate override.
32
+ _TEAM_BUILTINS = ('handoff', 'readShared', 'writeShared', 'listAgents')
33
+
34
+
35
+ def _team_tools(agent):
36
+ """Explicit tools first, then any team builtin the agent did not define."""
37
+ from quantum.runtime.agent_service import BUILTIN_TOOLS
38
+
39
+ tools = build_tool_definitions(agent.tools)
40
+ declared = {t.get('name') for t in tools}
41
+ for name in _TEAM_BUILTINS:
42
+ if name not in declared and name in BUILTIN_TOOLS:
43
+ tools.append(dict(BUILTIN_TOOLS[name]))
44
+ return tools
45
+
46
+
47
+ class TeamExecutor(BaseExecutor):
48
+ """
49
+ Executor for q:team statements.
50
+
51
+ Supports:
52
+ - Multi-agent orchestration
53
+ - Agent handoffs
54
+ - Shared team context
55
+ - Entry point routing
56
+ """
57
+
58
+ @property
59
+ def handles(self) -> List[Type]:
60
+ return [AgentTeamNode]
61
+
62
+ def execute(self, node: AgentTeamNode, exec_context) -> Any:
63
+ """
64
+ Execute agent team.
65
+
66
+ Args:
67
+ node: AgentTeamNode with team configuration
68
+ exec_context: Execution context
69
+
70
+ Returns:
71
+ Team execution result dict
72
+ """
73
+ try:
74
+ context = exec_context.get_all_variables()
75
+
76
+ # Resolve task from execute node
77
+ if not node.execute:
78
+ raise ExecutorError("Team requires <q:execute> to run")
79
+
80
+ task = self.apply_databinding(node.execute.task, context)
81
+
82
+ # Determine entry agent
83
+ entry_agent = node.execute.entry or node.supervisor
84
+ if not entry_agent and node.agents:
85
+ entry_agent = node.agents[0].name
86
+
87
+ # Initialize shared context
88
+ shared_context = {}
89
+ for set_node in node.shared:
90
+ if hasattr(set_node, 'name') and hasattr(set_node, 'value'):
91
+ value = self.apply_databinding(set_node.value, context)
92
+ shared_context[set_node.name] = value
93
+
94
+ # MultiAgentService.create_team() takes a {name: config} dict —
95
+ # not a list, and not the team_config dict this executor used to
96
+ # build. execute_team() also lives on services.multi_agent, not
97
+ # services.agent (which is the single-agent AgentService).
98
+ agents = {
99
+ agent.name: self._build_agent_config(agent, context)
100
+ for agent in node.agents
101
+ }
102
+
103
+ multi_agent = self.services.multi_agent
104
+ multi_agent.create_team(
105
+ name=node.name,
106
+ agents=agents,
107
+ shared=shared_context,
108
+ supervisor=node.supervisor or entry_agent,
109
+ max_handoffs=node.max_handoffs,
110
+ max_total_iterations=node.max_total_iterations,
111
+ )
112
+
113
+ team_result = multi_agent.execute_team(
114
+ name=node.name,
115
+ task=task,
116
+ entry_agent=entry_agent,
117
+ context="",
118
+ tool_executor=build_tool_executor(
119
+ self, exec_context,
120
+ [tool for agent in node.agents for tool in agent.tools],
121
+ ),
122
+ )
123
+
124
+ result = team_result.to_dict()
125
+
126
+ # Store results
127
+ exec_context.set_variable(node.name, result.get('finalResponse', ''), scope="component")
128
+ exec_context.set_variable(f"{node.name}_result", result, scope="component")
129
+
130
+ return result
131
+
132
+ except Exception as e:
133
+ error_result = {
134
+ 'success': False,
135
+ 'error': {'message': str(e)},
136
+ 'finalResponse': '',
137
+ 'handoffs': [],
138
+ 'totalIterations': 0
139
+ }
140
+ exec_context.set_variable(f"{node.name}_result", error_result, scope="component")
141
+ raise ExecutorError(f"Team execution error: {e}")
142
+
143
+ def _build_agent_config(self, agent, context: Dict[str, Any]) -> Dict[str, Any]:
144
+ """Build agent configuration dict from AgentNode.
145
+
146
+ Key names must match what AgentTeam.execute() reads:
147
+ instruction/model/endpoint/provider/api_key/max_iterations/timeout/tools.
148
+ """
149
+ instruction = ""
150
+ if agent.instruction:
151
+ instruction = self.apply_databinding(agent.instruction.content, context)
152
+
153
+ api_key = ""
154
+ if agent.api_key:
155
+ api_key = self.apply_databinding(agent.api_key, context)
156
+
157
+ endpoint = ""
158
+ if agent.endpoint:
159
+ endpoint = self.apply_databinding(agent.endpoint, context)
160
+
161
+ return {
162
+ 'name': agent.name,
163
+ 'model': agent.model,
164
+ 'provider': agent.provider,
165
+ 'endpoint': endpoint,
166
+ 'api_key': api_key,
167
+ 'instruction': instruction,
168
+ 'tools': _team_tools(agent),
169
+ 'max_iterations': agent.max_iterations,
170
+ 'timeout': agent.timeout,
171
+ }