quantalogic 0.2.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 (68) hide show
  1. quantalogic/__init__.py +20 -0
  2. quantalogic/agent.py +638 -0
  3. quantalogic/agent_config.py +138 -0
  4. quantalogic/coding_agent.py +83 -0
  5. quantalogic/event_emitter.py +223 -0
  6. quantalogic/generative_model.py +226 -0
  7. quantalogic/interactive_text_editor.py +190 -0
  8. quantalogic/main.py +185 -0
  9. quantalogic/memory.py +217 -0
  10. quantalogic/model_names.py +19 -0
  11. quantalogic/print_event.py +66 -0
  12. quantalogic/prompts.py +99 -0
  13. quantalogic/server/__init__.py +3 -0
  14. quantalogic/server/agent_server.py +633 -0
  15. quantalogic/server/models.py +60 -0
  16. quantalogic/server/routes.py +117 -0
  17. quantalogic/server/state.py +199 -0
  18. quantalogic/server/static/js/event_visualizer.js +430 -0
  19. quantalogic/server/static/js/quantalogic.js +571 -0
  20. quantalogic/server/templates/index.html +134 -0
  21. quantalogic/tool_manager.py +68 -0
  22. quantalogic/tools/__init__.py +46 -0
  23. quantalogic/tools/agent_tool.py +88 -0
  24. quantalogic/tools/download_http_file_tool.py +64 -0
  25. quantalogic/tools/edit_whole_content_tool.py +70 -0
  26. quantalogic/tools/elixir_tool.py +240 -0
  27. quantalogic/tools/execute_bash_command_tool.py +116 -0
  28. quantalogic/tools/input_question_tool.py +57 -0
  29. quantalogic/tools/language_handlers/__init__.py +21 -0
  30. quantalogic/tools/language_handlers/c_handler.py +33 -0
  31. quantalogic/tools/language_handlers/cpp_handler.py +33 -0
  32. quantalogic/tools/language_handlers/go_handler.py +33 -0
  33. quantalogic/tools/language_handlers/java_handler.py +37 -0
  34. quantalogic/tools/language_handlers/javascript_handler.py +42 -0
  35. quantalogic/tools/language_handlers/python_handler.py +29 -0
  36. quantalogic/tools/language_handlers/rust_handler.py +33 -0
  37. quantalogic/tools/language_handlers/scala_handler.py +33 -0
  38. quantalogic/tools/language_handlers/typescript_handler.py +42 -0
  39. quantalogic/tools/list_directory_tool.py +123 -0
  40. quantalogic/tools/llm_tool.py +119 -0
  41. quantalogic/tools/markitdown_tool.py +105 -0
  42. quantalogic/tools/nodejs_tool.py +515 -0
  43. quantalogic/tools/python_tool.py +469 -0
  44. quantalogic/tools/read_file_block_tool.py +140 -0
  45. quantalogic/tools/read_file_tool.py +79 -0
  46. quantalogic/tools/replace_in_file_tool.py +300 -0
  47. quantalogic/tools/ripgrep_tool.py +353 -0
  48. quantalogic/tools/search_definition_names.py +419 -0
  49. quantalogic/tools/task_complete_tool.py +35 -0
  50. quantalogic/tools/tool.py +146 -0
  51. quantalogic/tools/unified_diff_tool.py +387 -0
  52. quantalogic/tools/write_file_tool.py +97 -0
  53. quantalogic/utils/__init__.py +17 -0
  54. quantalogic/utils/ask_user_validation.py +12 -0
  55. quantalogic/utils/download_http_file.py +77 -0
  56. quantalogic/utils/get_coding_environment.py +15 -0
  57. quantalogic/utils/get_environment.py +26 -0
  58. quantalogic/utils/get_quantalogic_rules_content.py +19 -0
  59. quantalogic/utils/git_ls.py +121 -0
  60. quantalogic/utils/read_file.py +54 -0
  61. quantalogic/utils/read_http_text_content.py +101 -0
  62. quantalogic/xml_parser.py +242 -0
  63. quantalogic/xml_tool_parser.py +99 -0
  64. quantalogic-0.2.0.dist-info/LICENSE +201 -0
  65. quantalogic-0.2.0.dist-info/METADATA +1034 -0
  66. quantalogic-0.2.0.dist-info/RECORD +68 -0
  67. quantalogic-0.2.0.dist-info/WHEEL +4 -0
  68. quantalogic-0.2.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,515 @@
1
+ """Tool to execute Node.js scripts in an isolated Docker environment."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import subprocess
7
+ import tempfile
8
+
9
+ from quantalogic.tools.tool import Tool, ToolArgument
10
+
11
+ # Configure logging for the module
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class NodeJsTool(Tool):
16
+ """Tool to execute Node.js scripts in an isolated Docker environment."""
17
+
18
+ name: str = "nodejs_tool"
19
+ description: str = (
20
+ "Executes a Node.js script (ESM or CommonJS) within a Docker container using npm for package management.\n\n"
21
+ "CONSOLE OUTPUT REQUIREMENTS:\n"
22
+ "1. Only Node.js code that produces text output via console.log() statements is accepted\n"
23
+ "2. No GUI, no plots, no visualizations - strictly console/terminal output\n"
24
+ "3. No file operations or external resources unless explicitly authorized\n\n"
25
+ "EXECUTION ENVIRONMENT:\n"
26
+ "- Runs in an isolated Docker container\n"
27
+ "- Node.js version can be specified (default: Node.js LTS)\n"
28
+ "- Required packages can be installed via npm\n"
29
+ "- Standard Node.js modules are available\n\n"
30
+ "ACCEPTED OUTPUT METHODS:\n"
31
+ "✓ console.log()\n"
32
+ "✓ console.info()\n"
33
+ "✓ process.stdout.write()\n"
34
+ "✗ No browser-based output\n"
35
+ "✗ No external file generation\n"
36
+ "✗ No web servers or network services\n\n"
37
+ "EXAMPLE:\n"
38
+ "console.log('Hello, World!') # ✓ Valid\n"
39
+ "window.alert() # ✗ Invalid\n"
40
+ )
41
+ need_validation: bool = True
42
+ arguments: list[ToolArgument] = [
43
+ ToolArgument(
44
+ name="install_commands",
45
+ arg_type="string",
46
+ description=(
47
+ "Commands to install Node.js packages before running the script. "
48
+ "Use one command per line or separate packages with spaces."
49
+ ),
50
+ required=False,
51
+ example="npm install chalk axios",
52
+ ),
53
+ ToolArgument(
54
+ name="script",
55
+ arg_type="string",
56
+ description=(
57
+ "The Node.js script to execute. The script must print to the console. "
58
+ "Use import statements for ESM or require statements for CommonJS."
59
+ ),
60
+ required=True,
61
+ example='import fs from "fs";\nconsole.log("Hello, World!");\nconsole.log("This is a Node.js interpreter tool.");',
62
+ ),
63
+ ToolArgument(
64
+ name="version",
65
+ arg_type="string",
66
+ description=("The Node.js version to use in the Docker container. " "For example: '18', '20', 'lts'."),
67
+ required=True,
68
+ default="lts",
69
+ example="20",
70
+ ),
71
+ ToolArgument(
72
+ name="host_dir",
73
+ arg_type="string",
74
+ description=(
75
+ "The absolute path on the host machine to mount for file access. "
76
+ "Provide this path if you want to access files on the host."
77
+ ),
78
+ required=False,
79
+ default=os.getcwd(),
80
+ example="./project/",
81
+ ),
82
+ ToolArgument(
83
+ name="memory_limit",
84
+ arg_type="string",
85
+ description=(
86
+ "Optional memory limit for the Docker container (e.g., '512m', '2g'). "
87
+ "If not specified, Docker's default memory limit applies."
88
+ ),
89
+ required=False,
90
+ default=None,
91
+ example="1g",
92
+ ),
93
+ ToolArgument(
94
+ name="environment_vars",
95
+ arg_type="string",
96
+ description=(
97
+ "Environment variables to set inside the Docker container. "
98
+ "Provide as KEY=VALUE pairs separated by spaces."
99
+ ),
100
+ required=False,
101
+ default=None,
102
+ example="NODE_ENV=production DEBUG=false",
103
+ ),
104
+ # New Argument for Module Type
105
+ ToolArgument(
106
+ name="module_type",
107
+ arg_type="string",
108
+ description=("The module system to use: 'esm' for ECMAScript Modules or 'commonjs' for CommonJS."),
109
+ required=True,
110
+ default="esm",
111
+ example="commonjs",
112
+ ),
113
+ ]
114
+
115
+ def execute(
116
+ self,
117
+ install_commands: str | None = None,
118
+ script: str = "",
119
+ version: str = "lts",
120
+ host_dir: str | None = None,
121
+ memory_limit: str | None = None,
122
+ environment_vars: str | None = None,
123
+ module_type: str = "esm",
124
+ ) -> str:
125
+ """Executes a Node.js script (ESM or CommonJS) within a Docker container using npm for package management.
126
+
127
+ Args:
128
+ install_commands (str | None): Installation commands for dependencies.
129
+ script (str): The Node.js script to execute.
130
+ version (str): Node.js version to use.
131
+ host_dir (str | None): Host directory to mount for file access.
132
+ memory_limit (str | None): Memory limit for Docker container (e.g., '512m', '2g').
133
+ environment_vars (str | None): Environment variables for the Docker container.
134
+ module_type (str): The module system to use ('esm' or 'commonjs').
135
+
136
+ Returns:
137
+ str: The output of the executed script or error messages.
138
+
139
+ Raises:
140
+ ValueError: If the Node.js version is unsupported or inputs are invalid.
141
+ RuntimeError: If Docker commands fail or Docker is not available.
142
+ """
143
+ self._check_docker_availability()
144
+ self._validate_nodejs_version(version)
145
+ self._validate_module_type(module_type)
146
+
147
+ # Determine Docker image based on Node.js version
148
+ docker_image = f"node:{version}-slim"
149
+
150
+ if not self._is_docker_image_present(docker_image):
151
+ self._pull_docker_image(docker_image)
152
+
153
+ with tempfile.TemporaryDirectory() as temp_dir:
154
+ # Determine script filename based on module type
155
+ script_extension = "mjs" if module_type == "esm" else "cjs"
156
+ script_filename = f"script.{script_extension}"
157
+ script_path = os.path.join(temp_dir, script_filename)
158
+ self._write_script(script_path, script, module_type)
159
+
160
+ # Create package.json if installing dependencies or to specify module type
161
+ if install_commands or module_type:
162
+ self._create_package_json(temp_dir, module_type)
163
+
164
+ # Prepare npm install commands
165
+ npm_install_cmd = self._prepare_install_commands(install_commands)
166
+
167
+ # Run the Docker command and return the output
168
+ return self._run_docker_command(
169
+ docker_image=docker_image,
170
+ temp_dir=temp_dir,
171
+ host_dir=host_dir,
172
+ npm_install_cmd=npm_install_cmd,
173
+ memory_limit=memory_limit,
174
+ environment_vars=environment_vars,
175
+ script_filename=script_filename,
176
+ module_type=module_type,
177
+ )
178
+
179
+ def _validate_nodejs_version(self, version: str) -> None:
180
+ """Validates whether the specified Node.js version is supported.
181
+
182
+ Args:
183
+ version (str): Node.js version to validate.
184
+
185
+ Raises:
186
+ ValueError: If the Node.js version is unsupported.
187
+ """
188
+ valid_versions = ["16", "18", "20", "lts"]
189
+ if version not in valid_versions:
190
+ error_msg = f"Unsupported Node.js version '{version}'. " f"Supported versions: {', '.join(valid_versions)}."
191
+ logger.error(error_msg)
192
+ raise ValueError(error_msg)
193
+ logger.debug(f"Node.js version '{version}' is supported.")
194
+
195
+ def _validate_module_type(self, module_type: str) -> None:
196
+ """Validates whether the specified module type is supported.
197
+
198
+ Args:
199
+ module_type (str): Module type to validate.
200
+
201
+ Raises:
202
+ ValueError: If the module type is unsupported.
203
+ """
204
+ valid_module_types = ["esm", "commonjs"]
205
+ if module_type not in valid_module_types:
206
+ error_msg = (
207
+ f"Unsupported module type '{module_type}'. " f"Supported types: {', '.join(valid_module_types)}."
208
+ )
209
+ logger.error(error_msg)
210
+ raise ValueError(error_msg)
211
+ logger.debug(f"Module type '{module_type}' is supported.")
212
+
213
+ def _check_docker_availability(self) -> None:
214
+ """Checks if Docker is installed and accessible.
215
+
216
+ Raises:
217
+ RuntimeError: If Docker is not available.
218
+ """
219
+ try:
220
+ subprocess.run(
221
+ ["docker", "--version"],
222
+ check=True,
223
+ capture_output=True,
224
+ text=True,
225
+ )
226
+ logger.debug("Docker is installed and available.")
227
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
228
+ error_msg = "Docker is not installed or not accessible. Please install Docker and ensure it's running."
229
+ logger.error(error_msg)
230
+ raise RuntimeError(error_msg) from e
231
+
232
+ def _write_script(self, path: str, script: str, module_type: str) -> None:
233
+ """Writes the provided Node.js script to a specified file.
234
+
235
+ Args:
236
+ path (str): The path to write the script.
237
+ script (str): The content of the script.
238
+ module_type (str): The module system ('esm' or 'commonjs').
239
+
240
+ Raises:
241
+ ValueError: If the script content is empty.
242
+ """
243
+ if not script.strip():
244
+ error_msg = "The provided Node.js script is empty."
245
+ logger.error(error_msg)
246
+ raise ValueError(error_msg)
247
+
248
+ # Optional: Provide warnings if the script doesn't match the module type
249
+ if module_type == "esm":
250
+ if not any(script.lstrip().startswith(keyword) for keyword in ("import ", "export ")):
251
+ logger.warning("ESM module type selected, but the script does not contain import/export statements.")
252
+ elif module_type == "commonjs":
253
+ if "require(" not in script and not any(
254
+ script.lstrip().startswith(keyword) for keyword in ("const ", "let ", "var ")
255
+ ):
256
+ logger.warning("CommonJS module type selected, but the script does not contain require statements.")
257
+
258
+ with open(path, "w", encoding="utf-8") as script_file:
259
+ script_file.write(script)
260
+ logger.debug(f"Node.js script written to {path}")
261
+
262
+ def _create_package_json(self, dir_path: str, module_type: str) -> None:
263
+ """Creates a package.json file in the specified directory, specifying the module type.
264
+
265
+ Args:
266
+ dir_path (str): Directory where package.json should be created.
267
+ module_type (str): The module system ('esm' or 'commonjs').
268
+ """
269
+ package_json = {
270
+ "name": "script",
271
+ "version": "1.0.0",
272
+ "description": "Temporary Node.js script",
273
+ "main": f"script.{ 'mjs' if module_type == 'esm' else 'cjs' }",
274
+ "type": "module" if module_type == "esm" else "commonjs",
275
+ }
276
+
277
+ # For CommonJS, it's usually optional to set 'type', but in Node.js, 'type' defaults to CommonJS
278
+ # However, to ensure compatibility, we remove 'type' if 'commonjs' is chosen
279
+ if module_type == "commonjs":
280
+ del package_json["type"]
281
+
282
+ with open(os.path.join(dir_path, "package.json"), "w", encoding="utf-8") as f:
283
+ json.dump(package_json, f, indent=2)
284
+ logger.debug(f"Created package.json in {dir_path} with module type '{module_type}'.")
285
+
286
+ def _prepare_install_commands(self, install_commands: str | None) -> str:
287
+ """Prepares installation commands for npm.
288
+
289
+ Args:
290
+ install_commands (str | None): Installation commands provided by the user.
291
+
292
+ Returns:
293
+ str: A single npm install command string.
294
+ """
295
+ if install_commands:
296
+ packages = set()
297
+ for line in install_commands.splitlines():
298
+ parts = line.strip().split()
299
+ if parts and parts[0].lower() == "npm" and parts[1].lower() == "install":
300
+ packages.update(parts[2:])
301
+ else:
302
+ packages.update(parts)
303
+
304
+ if packages:
305
+ install_command = "npm install " + " ".join(packages)
306
+ logger.debug(f"Prepared npm install command: {install_command}")
307
+ return install_command
308
+
309
+ logger.debug("No installation commands provided.")
310
+ return ""
311
+
312
+ def _is_docker_image_present(self, docker_image: str) -> bool:
313
+ """Checks if the specified Docker image is already present locally.
314
+
315
+ Args:
316
+ docker_image (str): The Docker image to check.
317
+
318
+ Returns:
319
+ bool: True if the image is present, False otherwise.
320
+ """
321
+ try:
322
+ result = subprocess.run(
323
+ ["docker", "images", "-q", docker_image],
324
+ check=True,
325
+ capture_output=True,
326
+ text=True,
327
+ )
328
+ is_present = bool(result.stdout.strip())
329
+ logger.debug(f"Docker image '{docker_image}' present locally: {is_present}")
330
+ return is_present
331
+ except subprocess.CalledProcessError as e:
332
+ logger.error(f"Error checking Docker images: {e.stderr.strip()}")
333
+ return False
334
+
335
+ def _pull_docker_image(self, docker_image: str) -> None:
336
+ """Pulls the specified Docker image.
337
+
338
+ Args:
339
+ docker_image (str): The name of the Docker image to pull.
340
+
341
+ Raises:
342
+ RuntimeError: If pulling the Docker image fails.
343
+ """
344
+ try:
345
+ logger.info(f"Pulling Docker image: {docker_image}")
346
+ subprocess.run(
347
+ ["docker", "pull", docker_image],
348
+ check=True,
349
+ capture_output=True,
350
+ text=True,
351
+ )
352
+ logger.info(f"Successfully pulled Docker image '{docker_image}'.")
353
+ except subprocess.CalledProcessError as e:
354
+ error_msg = f"Failed to pull Docker image '{docker_image}': {e.stderr.strip()}"
355
+ logger.error(error_msg)
356
+ raise RuntimeError(error_msg) from e
357
+
358
+ def _parse_environment_vars(self, env_vars_str: str) -> dict:
359
+ """Parses environment variables from a string of KEY=VALUE pairs.
360
+
361
+ Args:
362
+ env_vars_str (str): Environment variables string.
363
+
364
+ Returns:
365
+ dict: Dictionary of environment variables.
366
+
367
+ Raises:
368
+ ValueError: If the environment variables string is malformed.
369
+ """
370
+ env_vars = {}
371
+ for pair in env_vars_str.split():
372
+ if "=" not in pair:
373
+ error_msg = f"Invalid environment variable format: '{pair}'. Expected 'KEY=VALUE'."
374
+ logger.error(error_msg)
375
+ raise ValueError(error_msg)
376
+
377
+ key, value = pair.split("=", 1)
378
+ env_vars[key] = value
379
+ logger.debug(f"Parsed environment variables: {env_vars}")
380
+ return env_vars
381
+
382
+ def _run_docker_command(
383
+ self,
384
+ docker_image: str,
385
+ temp_dir: str,
386
+ host_dir: str | None,
387
+ npm_install_cmd: str,
388
+ memory_limit: str | None,
389
+ environment_vars: str | None,
390
+ script_filename: str,
391
+ module_type: str,
392
+ ) -> str:
393
+ """Constructs and runs the Docker command to execute the Node.js script.
394
+
395
+ Args:
396
+ docker_image (str): The Docker image to use.
397
+ temp_dir (str): Temporary directory containing the script.
398
+ host_dir (str | None): Host directory to mount, or None to run without it.
399
+ npm_install_cmd (str): Command string for installing packages.
400
+ memory_limit (str | None): Memory limit for Docker container.
401
+ environment_vars (str | None): Environment variables for the Docker container.
402
+ script_filename (str): The name of the script file.
403
+ module_type (str): The module system ('esm' or 'commonjs').
404
+
405
+ Returns:
406
+ str: The output from executing the command.
407
+
408
+ Raises:
409
+ RuntimeError: If executing the Docker command fails.
410
+ """
411
+ docker_run_cmd = [
412
+ "docker",
413
+ "run",
414
+ "--rm",
415
+ "-v",
416
+ f"{temp_dir}:/usr/src/app",
417
+ "-w",
418
+ "/usr/src/app",
419
+ ]
420
+
421
+ if host_dir:
422
+ if not os.path.isdir(host_dir):
423
+ error_msg = f"Host directory '{host_dir}' does not exist or is not a directory."
424
+ logger.error(error_msg)
425
+ raise ValueError(error_msg)
426
+ docker_run_cmd += ["-v", f"{os.path.abspath(host_dir)}:/usr/src/host_data"]
427
+
428
+ if memory_limit:
429
+ docker_run_cmd += ["-m", memory_limit]
430
+
431
+ if environment_vars:
432
+ env_pairs = self._parse_environment_vars(environment_vars)
433
+ for key, value in env_pairs.items():
434
+ docker_run_cmd += ["-e", f"{key}={value}"]
435
+
436
+ docker_run_cmd.append(docker_image)
437
+
438
+ # Determine the command to run based on module type
439
+ if npm_install_cmd:
440
+ command_with_install = f"{npm_install_cmd} 1>2 && node {script_filename}"
441
+ docker_run_cmd += ["sh", "-c", command_with_install]
442
+ else:
443
+ docker_run_cmd += ["node", script_filename]
444
+
445
+ logger.debug(f"Docker run command: {' '.join(docker_run_cmd)}")
446
+
447
+ try:
448
+ result = subprocess.run(
449
+ docker_run_cmd,
450
+ check=True,
451
+ capture_output=True,
452
+ text=True,
453
+ timeout=300,
454
+ )
455
+ logger.debug(f"Docker command stdout: {result.stdout}")
456
+ logger.debug(f"Docker command stderr: {result.stderr}")
457
+ return result.stdout
458
+ except subprocess.CalledProcessError as e:
459
+ error_msg = (
460
+ f"Docker command failed with return code {e.returncode}.\n"
461
+ f"Docker Command: {' '.join(docker_run_cmd)}\n"
462
+ f"Standard Output:\n{e.stdout}\n"
463
+ f"Standard Error:\n{e.stderr}"
464
+ )
465
+ logger.error(error_msg)
466
+ raise RuntimeError(error_msg) from e
467
+ except subprocess.TimeoutExpired as e:
468
+ error_msg = "Docker command timed out."
469
+ logger.error(error_msg)
470
+ raise RuntimeError(error_msg) from e
471
+
472
+
473
+ if __name__ == "__main__":
474
+ # Example usage of NodeJsTool
475
+ tool = NodeJsTool()
476
+ install_commands = "npm install chalk"
477
+
478
+ # Example ESM script
479
+ esm_script = """\
480
+ import chalk from 'chalk';
481
+ console.log(chalk.blue('Hello, ESM World!'));
482
+ console.log('This is a Node.js interpreter tool using ESM.');
483
+ """
484
+
485
+ # Example CommonJS script
486
+ commonjs_script = """\
487
+ const chalk = require('chalk');
488
+ console.log(chalk.green('Hello, CommonJS World!'));
489
+ console.log('This is a Node.js interpreter tool using CommonJS.');
490
+ """
491
+
492
+ version = "20"
493
+ host_directory = None
494
+ memory_limit = "1g"
495
+ environment_variables = "NODE_ENV=production DEBUG=false"
496
+
497
+ # Choose the module type and corresponding script
498
+ module_type = "esm" # Change to "commonjs" for CommonJS scripts
499
+ script = esm_script if module_type == "esm" else commonjs_script
500
+
501
+ try:
502
+ output = tool.execute(
503
+ install_commands=install_commands,
504
+ script=script,
505
+ version=version,
506
+ host_dir=host_directory,
507
+ memory_limit=memory_limit,
508
+ environment_vars=environment_variables,
509
+ module_type=module_type,
510
+ )
511
+ print("Script Output:")
512
+ print(output)
513
+ except Exception as e:
514
+ logger.error(f"An error occurred during script execution: {e}")
515
+ print(f"An error occurred: {e}")