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.
- quantalogic/__init__.py +20 -0
- quantalogic/agent.py +638 -0
- quantalogic/agent_config.py +138 -0
- quantalogic/coding_agent.py +83 -0
- quantalogic/event_emitter.py +223 -0
- quantalogic/generative_model.py +226 -0
- quantalogic/interactive_text_editor.py +190 -0
- quantalogic/main.py +185 -0
- quantalogic/memory.py +217 -0
- quantalogic/model_names.py +19 -0
- quantalogic/print_event.py +66 -0
- quantalogic/prompts.py +99 -0
- quantalogic/server/__init__.py +3 -0
- quantalogic/server/agent_server.py +633 -0
- quantalogic/server/models.py +60 -0
- quantalogic/server/routes.py +117 -0
- quantalogic/server/state.py +199 -0
- quantalogic/server/static/js/event_visualizer.js +430 -0
- quantalogic/server/static/js/quantalogic.js +571 -0
- quantalogic/server/templates/index.html +134 -0
- quantalogic/tool_manager.py +68 -0
- quantalogic/tools/__init__.py +46 -0
- quantalogic/tools/agent_tool.py +88 -0
- quantalogic/tools/download_http_file_tool.py +64 -0
- quantalogic/tools/edit_whole_content_tool.py +70 -0
- quantalogic/tools/elixir_tool.py +240 -0
- quantalogic/tools/execute_bash_command_tool.py +116 -0
- quantalogic/tools/input_question_tool.py +57 -0
- quantalogic/tools/language_handlers/__init__.py +21 -0
- quantalogic/tools/language_handlers/c_handler.py +33 -0
- quantalogic/tools/language_handlers/cpp_handler.py +33 -0
- quantalogic/tools/language_handlers/go_handler.py +33 -0
- quantalogic/tools/language_handlers/java_handler.py +37 -0
- quantalogic/tools/language_handlers/javascript_handler.py +42 -0
- quantalogic/tools/language_handlers/python_handler.py +29 -0
- quantalogic/tools/language_handlers/rust_handler.py +33 -0
- quantalogic/tools/language_handlers/scala_handler.py +33 -0
- quantalogic/tools/language_handlers/typescript_handler.py +42 -0
- quantalogic/tools/list_directory_tool.py +123 -0
- quantalogic/tools/llm_tool.py +119 -0
- quantalogic/tools/markitdown_tool.py +105 -0
- quantalogic/tools/nodejs_tool.py +515 -0
- quantalogic/tools/python_tool.py +469 -0
- quantalogic/tools/read_file_block_tool.py +140 -0
- quantalogic/tools/read_file_tool.py +79 -0
- quantalogic/tools/replace_in_file_tool.py +300 -0
- quantalogic/tools/ripgrep_tool.py +353 -0
- quantalogic/tools/search_definition_names.py +419 -0
- quantalogic/tools/task_complete_tool.py +35 -0
- quantalogic/tools/tool.py +146 -0
- quantalogic/tools/unified_diff_tool.py +387 -0
- quantalogic/tools/write_file_tool.py +97 -0
- quantalogic/utils/__init__.py +17 -0
- quantalogic/utils/ask_user_validation.py +12 -0
- quantalogic/utils/download_http_file.py +77 -0
- quantalogic/utils/get_coding_environment.py +15 -0
- quantalogic/utils/get_environment.py +26 -0
- quantalogic/utils/get_quantalogic_rules_content.py +19 -0
- quantalogic/utils/git_ls.py +121 -0
- quantalogic/utils/read_file.py +54 -0
- quantalogic/utils/read_http_text_content.py +101 -0
- quantalogic/xml_parser.py +242 -0
- quantalogic/xml_tool_parser.py +99 -0
- quantalogic-0.2.0.dist-info/LICENSE +201 -0
- quantalogic-0.2.0.dist-info/METADATA +1034 -0
- quantalogic-0.2.0.dist-info/RECORD +68 -0
- quantalogic-0.2.0.dist-info/WHEEL +4 -0
- quantalogic-0.2.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,469 @@
|
|
1
|
+
"""Tool to execute Python scripts in an isolated Docker environment."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
import subprocess
|
6
|
+
import tempfile
|
7
|
+
|
8
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
9
|
+
|
10
|
+
# Configure logging for the module
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
class PythonTool(Tool):
|
15
|
+
"""Tool to execute Python scripts in an isolated Docker environment."""
|
16
|
+
|
17
|
+
name: str = "python_tool"
|
18
|
+
description: str = (
|
19
|
+
"Executes a Python 3.11 script that print statements on the console within a Docker container using pip for package management.\n\n"
|
20
|
+
"CONSOLE OUTPUT REQUIREMENTS:\n"
|
21
|
+
"1. Only Python code that produces text output via print() statements is accepted\n"
|
22
|
+
"2. No GUI, no plots, no visualizations - strictly console/terminal output\n"
|
23
|
+
"3. No file operations or external resources unless explicitly authorized\n\n"
|
24
|
+
"EXECUTION ENVIRONMENT:\n"
|
25
|
+
"- Runs in an isolated Docker container\n"
|
26
|
+
"- Python version can be specified (default: Python 3.x)\n"
|
27
|
+
"- Required packages can be installed via pip\n"
|
28
|
+
"- Standard library modules are available\n\n"
|
29
|
+
"- Host directory mounting\n"
|
30
|
+
" - If provided, the host directory is mounted inside the Docker container at /usr/src/host_data\n"
|
31
|
+
" - This allows access to files on the host of the user\n"
|
32
|
+
"- Memory limit configuration\n"
|
33
|
+
" - The memory limit can be configured for the Docker container\n"
|
34
|
+
"- Network access\n"
|
35
|
+
" - The Docker container has full network access\n\n"
|
36
|
+
"ACCEPTED OUTPUT METHODS:\n"
|
37
|
+
"✓ print()\n"
|
38
|
+
"✓ sys.stdout.write()\n"
|
39
|
+
"✗ No matplotlib, tkinter, or other GUI libraries\n"
|
40
|
+
"✗ No external file generation\n"
|
41
|
+
"✗ No web servers or network services\n\n"
|
42
|
+
"EXAMPLE:\n"
|
43
|
+
"print('Hello, World!') # ✓ Valid\n"
|
44
|
+
"plt.show() # ✗ Invalid\n"
|
45
|
+
)
|
46
|
+
need_validation: bool = True
|
47
|
+
arguments: list[ToolArgument] = [
|
48
|
+
ToolArgument(
|
49
|
+
name="install_commands",
|
50
|
+
arg_type="string",
|
51
|
+
description=(
|
52
|
+
"Commands to install Python packages before running the script. "
|
53
|
+
"Use one command per line or separate packages with spaces."
|
54
|
+
),
|
55
|
+
required=False,
|
56
|
+
example="pip install rich requests",
|
57
|
+
),
|
58
|
+
ToolArgument(
|
59
|
+
name="script",
|
60
|
+
arg_type="string",
|
61
|
+
description=(
|
62
|
+
"The Python script to execute."
|
63
|
+
"The script must use /usr/src/host_data/ as the working directory."
|
64
|
+
"Host data is the directory provided in the host_dir argument."
|
65
|
+
"The script must produce text output via print() statements."
|
66
|
+
),
|
67
|
+
required=True,
|
68
|
+
example='print("Hello, World!")\nprint("This is a Python interpreter tool.")',
|
69
|
+
),
|
70
|
+
ToolArgument(
|
71
|
+
name="version",
|
72
|
+
arg_type="string",
|
73
|
+
description=("The Python version to use in the Docker container. " "For example: '3.11', '3.12'."),
|
74
|
+
required=True,
|
75
|
+
default="3.11",
|
76
|
+
example="3.11",
|
77
|
+
),
|
78
|
+
ToolArgument(
|
79
|
+
name="host_dir",
|
80
|
+
arg_type="string",
|
81
|
+
description=(
|
82
|
+
"The absolute path on the host machine to mount for file access. "
|
83
|
+
"Provide this path if you want to access files on the host of the user."
|
84
|
+
),
|
85
|
+
required=True,
|
86
|
+
default=None,
|
87
|
+
example="./demo01/",
|
88
|
+
),
|
89
|
+
ToolArgument(
|
90
|
+
name="memory_limit",
|
91
|
+
arg_type="string",
|
92
|
+
description=(
|
93
|
+
"Optional memory limit for the Docker container (e.g., '512m', '2g'). "
|
94
|
+
"If not specified, Docker's default memory limit applies."
|
95
|
+
),
|
96
|
+
required=False,
|
97
|
+
default=None,
|
98
|
+
example="1g",
|
99
|
+
),
|
100
|
+
ToolArgument(
|
101
|
+
name="environment_vars",
|
102
|
+
arg_type="string",
|
103
|
+
description=(
|
104
|
+
"Environment variables to set inside the Docker container. "
|
105
|
+
"Provide as KEY=VALUE pairs separated by spaces."
|
106
|
+
),
|
107
|
+
required=False,
|
108
|
+
default=None,
|
109
|
+
example="ENV=production DEBUG=False",
|
110
|
+
),
|
111
|
+
]
|
112
|
+
|
113
|
+
def execute(
|
114
|
+
self,
|
115
|
+
install_commands: str | None = None,
|
116
|
+
script: str = "",
|
117
|
+
version: str = "3.12",
|
118
|
+
host_dir: str | None = None,
|
119
|
+
memory_limit: str | None = None,
|
120
|
+
environment_vars: str | None = None,
|
121
|
+
) -> str:
|
122
|
+
"""Executes a Python script within a Docker container using pip for package management.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
install_commands (str | None): Installation commands for dependencies.
|
126
|
+
script (str): The Python script to execute.
|
127
|
+
version (str): Python version to use.
|
128
|
+
host_dir (str | None): Host directory to mount for file access.
|
129
|
+
memory_limit (str | None): Memory limit for Docker container (e.g., '512m', '2g').
|
130
|
+
environment_vars (str | None): Environment variables for the Docker container.
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
str: The output of the executed script or error messages.
|
134
|
+
|
135
|
+
Raises:
|
136
|
+
ValueError: If the Python version is unsupported or inputs are invalid.
|
137
|
+
RuntimeError: If Docker commands fail or Docker is not available.
|
138
|
+
"""
|
139
|
+
# Validate inputs
|
140
|
+
self._check_docker_availability()
|
141
|
+
self._validate_python_version(version)
|
142
|
+
|
143
|
+
# Prepare Docker image based on Python version
|
144
|
+
# Use the new uv Docker image family
|
145
|
+
docker_image = "ghcr.io/astral-sh/uv:bookworm"
|
146
|
+
|
147
|
+
# Ensure the Docker image is available locally
|
148
|
+
if not self._is_docker_image_present(docker_image):
|
149
|
+
self._pull_docker_image(docker_image)
|
150
|
+
|
151
|
+
# Create a temporary directory to store the script
|
152
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
153
|
+
# Write the script to a file
|
154
|
+
script_path = os.path.join(temp_dir, "script.py")
|
155
|
+
self._write_script(script_path, script)
|
156
|
+
|
157
|
+
# Prepare pip install commands
|
158
|
+
pip_install_cmd = self._prepare_install_commands(install_commands)
|
159
|
+
|
160
|
+
# Run the Docker command and return the output
|
161
|
+
return self._run_docker_command(
|
162
|
+
docker_image=docker_image,
|
163
|
+
temp_dir=temp_dir,
|
164
|
+
host_dir=host_dir,
|
165
|
+
pip_install_cmd=pip_install_cmd,
|
166
|
+
memory_limit=memory_limit,
|
167
|
+
environment_vars=environment_vars,
|
168
|
+
)
|
169
|
+
|
170
|
+
def _validate_python_version(self, version: str) -> None:
|
171
|
+
"""Validates whether the specified Python version is supported.
|
172
|
+
|
173
|
+
Args:
|
174
|
+
version (str): Python version to validate.
|
175
|
+
|
176
|
+
Raises:
|
177
|
+
ValueError: If the Python version is unsupported.
|
178
|
+
"""
|
179
|
+
valid_versions = ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
180
|
+
if version not in valid_versions:
|
181
|
+
error_msg = f"Unsupported Python version '{version}'. " f"Supported versions: {', '.join(valid_versions)}."
|
182
|
+
logger.error(error_msg)
|
183
|
+
raise ValueError(error_msg)
|
184
|
+
logger.debug(f"Python version '{version}' is supported.")
|
185
|
+
|
186
|
+
def _check_docker_availability(self) -> None:
|
187
|
+
"""Checks if Docker is installed and accessible.
|
188
|
+
|
189
|
+
Raises:
|
190
|
+
RuntimeError: If Docker is not available.
|
191
|
+
"""
|
192
|
+
try:
|
193
|
+
subprocess.run(
|
194
|
+
["docker", "--version"],
|
195
|
+
check=True,
|
196
|
+
capture_output=True,
|
197
|
+
text=True,
|
198
|
+
)
|
199
|
+
logger.debug("Docker is installed and available.")
|
200
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
201
|
+
error_msg = "Docker is not installed or not accessible. Please install Docker and ensure it's running."
|
202
|
+
logger.error(error_msg)
|
203
|
+
raise RuntimeError(error_msg) from e
|
204
|
+
|
205
|
+
def _write_script(self, path: str, script: str) -> None:
|
206
|
+
"""Writes the provided Python script to a specified file.
|
207
|
+
|
208
|
+
Args:
|
209
|
+
path (str): The path to write the script.
|
210
|
+
script (str): The content of the script.
|
211
|
+
|
212
|
+
Raises:
|
213
|
+
ValueError: If the script content is empty.
|
214
|
+
"""
|
215
|
+
if not script.strip():
|
216
|
+
error_msg = "The provided Python script is empty."
|
217
|
+
logger.error(error_msg)
|
218
|
+
raise ValueError(error_msg)
|
219
|
+
|
220
|
+
with open(path, "w", encoding="utf-8") as script_file:
|
221
|
+
script_file.write(script)
|
222
|
+
logger.debug(f"Python script written to {path}")
|
223
|
+
|
224
|
+
def _prepare_install_commands(self, install_commands: str | None) -> str:
|
225
|
+
"""Prepares installation commands for pip.
|
226
|
+
|
227
|
+
Args:
|
228
|
+
install_commands (str | None): Installation commands provided by the user.
|
229
|
+
|
230
|
+
Returns:
|
231
|
+
str: A single pip install command string.
|
232
|
+
"""
|
233
|
+
if install_commands:
|
234
|
+
packages = set() # Use a set to handle duplicates
|
235
|
+
for line in install_commands.splitlines():
|
236
|
+
parts = line.strip().split()
|
237
|
+
if parts and parts[0].lower() == "pip" and parts[1].lower() == "install":
|
238
|
+
packages.update(parts[2:]) # Add all packages after "pip install"
|
239
|
+
else:
|
240
|
+
packages.update(parts)
|
241
|
+
|
242
|
+
if packages:
|
243
|
+
install_command = "uv venv && source .venv/bin/activate && uv pip install --upgrade pip " + " ".join(
|
244
|
+
packages
|
245
|
+
)
|
246
|
+
logger.debug(f"Prepared pip install command: {install_command}")
|
247
|
+
return install_command
|
248
|
+
|
249
|
+
logger.debug("No installation commands provided.")
|
250
|
+
return ""
|
251
|
+
|
252
|
+
def _pull_docker_image(self, docker_image: str) -> None:
|
253
|
+
"""Pulls the specified Docker image.
|
254
|
+
|
255
|
+
Args:
|
256
|
+
docker_image (str): The name of the Docker image to pull.
|
257
|
+
|
258
|
+
Raises:
|
259
|
+
RuntimeError: If pulling the Docker image fails.
|
260
|
+
"""
|
261
|
+
try:
|
262
|
+
logger.info(f"Pulling Docker image: {docker_image}")
|
263
|
+
subprocess.run(
|
264
|
+
["docker", "pull", docker_image],
|
265
|
+
check=True,
|
266
|
+
capture_output=True,
|
267
|
+
text=True,
|
268
|
+
)
|
269
|
+
logger.info(f"Successfully pulled Docker image '{docker_image}'.")
|
270
|
+
except subprocess.CalledProcessError as e:
|
271
|
+
error_msg = f"Failed to pull Docker image '{docker_image}': {e.stderr.strip()}"
|
272
|
+
logger.error(error_msg)
|
273
|
+
raise RuntimeError(error_msg) from e
|
274
|
+
|
275
|
+
def _is_docker_image_present(self, docker_image: str) -> bool:
|
276
|
+
"""Checks if the specified Docker image is already present locally.
|
277
|
+
|
278
|
+
Args:
|
279
|
+
docker_image (str): The Docker image to check.
|
280
|
+
|
281
|
+
Returns:
|
282
|
+
bool: True if the image is present, False otherwise.
|
283
|
+
"""
|
284
|
+
try:
|
285
|
+
result = subprocess.run(
|
286
|
+
["docker", "images", "-q", docker_image],
|
287
|
+
check=True,
|
288
|
+
capture_output=True,
|
289
|
+
text=True,
|
290
|
+
)
|
291
|
+
is_present = bool(result.stdout.strip())
|
292
|
+
logger.debug(f"Docker image '{docker_image}' present locally: {is_present}")
|
293
|
+
return is_present
|
294
|
+
except subprocess.CalledProcessError as e:
|
295
|
+
logger.error(f"Error checking Docker images: {e.stderr.strip()}")
|
296
|
+
return False
|
297
|
+
|
298
|
+
def _run_docker_command(
|
299
|
+
self,
|
300
|
+
docker_image: str,
|
301
|
+
temp_dir: str,
|
302
|
+
host_dir: str | None,
|
303
|
+
pip_install_cmd: str,
|
304
|
+
memory_limit: str | None,
|
305
|
+
environment_vars: str | None,
|
306
|
+
) -> str:
|
307
|
+
"""Constructs and runs the Docker command to execute the Python script.
|
308
|
+
|
309
|
+
Args:
|
310
|
+
docker_image (str): The Docker image to use.
|
311
|
+
temp_dir (str): Temporary directory containing the script.
|
312
|
+
host_dir (str | None): Host directory to mount, or None to run without it.
|
313
|
+
pip_install_cmd (str): Command string for installing packages.
|
314
|
+
memory_limit (str | None): Memory limit for Docker container.
|
315
|
+
environment_vars (str | None): Environment variables for the Docker container.
|
316
|
+
|
317
|
+
Returns:
|
318
|
+
str: The output from executing the command.
|
319
|
+
|
320
|
+
Raises:
|
321
|
+
RuntimeError: If executing the Docker command fails.
|
322
|
+
"""
|
323
|
+
docker_run_cmd = [
|
324
|
+
"docker",
|
325
|
+
"run",
|
326
|
+
"--rm",
|
327
|
+
"-v",
|
328
|
+
f"{temp_dir}:/usr/src/app", # Mount temporary directory for scripts
|
329
|
+
"-w",
|
330
|
+
"/usr/src/app",
|
331
|
+
]
|
332
|
+
|
333
|
+
# Handle optional host directory mounting
|
334
|
+
if host_dir:
|
335
|
+
if not os.path.isdir(host_dir):
|
336
|
+
error_msg = f"Host directory '{host_dir}' does not exist or is not a directory."
|
337
|
+
logger.error(error_msg)
|
338
|
+
raise ValueError(error_msg)
|
339
|
+
docker_run_cmd += ["-v", f"{os.path.abspath(host_dir)}:/usr/src/host_data"]
|
340
|
+
docker_run_cmd += ["-v", f"{os.path.abspath(host_dir)}:{os.path.abspath(host_dir)}"]
|
341
|
+
|
342
|
+
# Apply memory limit if specified
|
343
|
+
if memory_limit:
|
344
|
+
docker_run_cmd += ["-m", memory_limit]
|
345
|
+
logger.debug(f"Setting Docker memory limit: {memory_limit}")
|
346
|
+
|
347
|
+
# Set environment variables if provided
|
348
|
+
if environment_vars:
|
349
|
+
env_pairs = self._parse_environment_vars(environment_vars)
|
350
|
+
for key, value in env_pairs.items():
|
351
|
+
docker_run_cmd += ["-e", f"{key}={value}"]
|
352
|
+
logger.debug(f"Setting Docker environment variables: {env_pairs}")
|
353
|
+
|
354
|
+
# Specify the Docker image and command to execute
|
355
|
+
docker_run_cmd.append(docker_image)
|
356
|
+
|
357
|
+
# Construct the command to execute inside the container
|
358
|
+
if pip_install_cmd:
|
359
|
+
command_with_install = f"{pip_install_cmd} && python3 script.py"
|
360
|
+
docker_run_cmd += ["bash", "-c", command_with_install]
|
361
|
+
logger.debug("Added installation and execution commands to Docker run command.")
|
362
|
+
else:
|
363
|
+
# Use bash -c to execute shell commands properly
|
364
|
+
venv_and_run = "uv venv && . .venv/bin/activate && python3 script.py"
|
365
|
+
docker_run_cmd += ["bash", "-c", venv_and_run]
|
366
|
+
logger.debug("Added script execution command to Docker run command.")
|
367
|
+
|
368
|
+
logger.info(f"Executing Docker command: {' '.join(docker_run_cmd)}")
|
369
|
+
try:
|
370
|
+
result = subprocess.run(
|
371
|
+
docker_run_cmd,
|
372
|
+
check=True,
|
373
|
+
capture_output=True,
|
374
|
+
text=True,
|
375
|
+
timeout=300,
|
376
|
+
)
|
377
|
+
logger.debug("Docker command executed successfully.")
|
378
|
+
result = result.stdout.strip()
|
379
|
+
if result == "":
|
380
|
+
result = "Script executed successfully."
|
381
|
+
return result
|
382
|
+
except subprocess.CalledProcessError as e:
|
383
|
+
error_msg = (
|
384
|
+
f"Docker command failed with return code {e.returncode}.\n"
|
385
|
+
f"Docker Command: {' '.join(docker_run_cmd)}\n"
|
386
|
+
f"Standard Output:\n{e.stdout}\n"
|
387
|
+
f"Standard Error:\n{e.stderr}"
|
388
|
+
)
|
389
|
+
logger.error(error_msg)
|
390
|
+
raise RuntimeError(error_msg) from e
|
391
|
+
except subprocess.TimeoutExpired as e:
|
392
|
+
error_msg = "Docker command timed out."
|
393
|
+
logger.error(error_msg)
|
394
|
+
raise RuntimeError(error_msg) from e
|
395
|
+
|
396
|
+
def _parse_environment_vars(self, env_vars_str: str) -> dict:
|
397
|
+
"""Parses environment variables from a string of KEY=VALUE pairs.
|
398
|
+
|
399
|
+
Args:
|
400
|
+
env_vars_str (str): Environment variables string.
|
401
|
+
|
402
|
+
Returns:
|
403
|
+
dict: Dictionary of environment variables.
|
404
|
+
|
405
|
+
Raises:
|
406
|
+
ValueError: If the environment variables string is malformed.
|
407
|
+
"""
|
408
|
+
env_vars = {}
|
409
|
+
for pair in env_vars_str.split():
|
410
|
+
if "=" not in pair:
|
411
|
+
error_msg = f"Invalid environment variable format: '{pair}'. Expected 'KEY=VALUE'."
|
412
|
+
logger.error(error_msg)
|
413
|
+
raise ValueError(error_msg)
|
414
|
+
|
415
|
+
key, value = pair.split("=", 1)
|
416
|
+
env_vars[key] = value
|
417
|
+
logger.debug(f"Parsed environment variables: {env_vars}")
|
418
|
+
return env_vars
|
419
|
+
|
420
|
+
|
421
|
+
if __name__ == "__main__":
|
422
|
+
# Example usage of PythonTool
|
423
|
+
tool = PythonTool()
|
424
|
+
install_commands = "pip install rich requests"
|
425
|
+
script = """\
|
426
|
+
from rich import print
|
427
|
+
print("Hello, World!")
|
428
|
+
print("This is a Python interpreter tool.")
|
429
|
+
"""
|
430
|
+
version = "3.12"
|
431
|
+
host_directory = None # Replace with actual path if needed
|
432
|
+
memory_limit = "1g" # Example: '512m', '2g'
|
433
|
+
environment_variables = "ENV=production DEBUG=False"
|
434
|
+
|
435
|
+
try:
|
436
|
+
output = tool.execute(
|
437
|
+
install_commands=install_commands,
|
438
|
+
script=script,
|
439
|
+
version=version,
|
440
|
+
host_dir=host_directory,
|
441
|
+
memory_limit=memory_limit,
|
442
|
+
environment_vars=environment_variables,
|
443
|
+
)
|
444
|
+
print("Script Output:")
|
445
|
+
print(output)
|
446
|
+
except Exception as e:
|
447
|
+
logger.error(f"An error occurred during script execution: {e}")
|
448
|
+
print(f"An error occurred: {e}")
|
449
|
+
|
450
|
+
# Example of writing to host directory
|
451
|
+
tool = PythonTool()
|
452
|
+
host_directory = "/usr/src/host_data" # Path inside container mapped to demo03/files
|
453
|
+
script = """\
|
454
|
+
# Write a sample text file to the host directory
|
455
|
+
with open('/usr/src/host_data/sample.txt', 'w') as f:
|
456
|
+
f.write('This is a sample text file created by PythonTool\\n')
|
457
|
+
print('Successfully wrote sample.txt to host directory')
|
458
|
+
"""
|
459
|
+
try:
|
460
|
+
output = tool.execute(
|
461
|
+
script=script,
|
462
|
+
version="3.12",
|
463
|
+
host_dir="./demo03/files",
|
464
|
+
)
|
465
|
+
print("File Write Output:")
|
466
|
+
print(output)
|
467
|
+
except Exception as e:
|
468
|
+
logger.error(f"An error occurred during file write: {e}")
|
469
|
+
print(f"An error occurred: {e}")
|
@@ -0,0 +1,140 @@
|
|
1
|
+
"""Tool for reading a block of lines from a file."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
|
5
|
+
from pydantic import field_validator
|
6
|
+
|
7
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
8
|
+
|
9
|
+
MAX_LINES = 200
|
10
|
+
|
11
|
+
|
12
|
+
class ReadFileBlockTool(Tool):
|
13
|
+
"""Tool for reading a block of lines from a file."""
|
14
|
+
|
15
|
+
name: str = "read_file_block_tool"
|
16
|
+
description: str = (
|
17
|
+
"Reads a block of lines from a file and returns its content."
|
18
|
+
"Good to read specific portions of a file. But not adapted when the full file is needed."
|
19
|
+
f"Can return only a max of {MAX_LINES} lines at a time."
|
20
|
+
"Use multiple read_file_block_tool to read larger files."
|
21
|
+
)
|
22
|
+
arguments: list = [
|
23
|
+
ToolArgument(
|
24
|
+
name="file_path",
|
25
|
+
arg_type="string",
|
26
|
+
description="The path to the file to read.",
|
27
|
+
required=True,
|
28
|
+
example="/path/to/file.txt",
|
29
|
+
),
|
30
|
+
ToolArgument(
|
31
|
+
name="line_start",
|
32
|
+
arg_type="int",
|
33
|
+
description="The starting line number (1-based index).",
|
34
|
+
required=True,
|
35
|
+
example="10",
|
36
|
+
),
|
37
|
+
ToolArgument(
|
38
|
+
name="line_end",
|
39
|
+
arg_type="int",
|
40
|
+
description="The ending line number (1-based index).",
|
41
|
+
required=True,
|
42
|
+
example="200",
|
43
|
+
),
|
44
|
+
]
|
45
|
+
|
46
|
+
@field_validator("line_start", "line_end", check_fields=False)
|
47
|
+
@classmethod
|
48
|
+
def validate_line_numbers(cls, v: int, info) -> int:
|
49
|
+
"""Validate that line_start and line_end are positive integers and line_start <= line_end."""
|
50
|
+
if not isinstance(v, int):
|
51
|
+
raise ValueError("Line numbers must be integers.")
|
52
|
+
|
53
|
+
if v <= 0:
|
54
|
+
raise ValueError("Line numbers must be positive integers.")
|
55
|
+
|
56
|
+
# If both line_start and line_end are being validated
|
57
|
+
if info.data and len(info.data) >= 2:
|
58
|
+
line_start = info.data.get("line_start")
|
59
|
+
line_end = info.data.get("line_end")
|
60
|
+
|
61
|
+
if line_start is not None and line_end is not None and line_start > line_end:
|
62
|
+
raise ValueError("line_start must be less than or equal to line_end.")
|
63
|
+
|
64
|
+
return v
|
65
|
+
|
66
|
+
def execute(self, file_path: str, line_start: int, line_end: int) -> str:
|
67
|
+
"""Reads a block of lines from a file and returns its content.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
file_path (str): The path to the file to read.
|
71
|
+
line_start (int): The starting line number (1-based index).
|
72
|
+
line_end (int): The ending line number (1-based index).
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
str: The content of the specified block of lines.
|
76
|
+
|
77
|
+
Raises:
|
78
|
+
ValueError: If line numbers are invalid or file cannot be read
|
79
|
+
FileNotFoundError: If the file does not exist
|
80
|
+
PermissionError: If there are permission issues reading the file
|
81
|
+
"""
|
82
|
+
try:
|
83
|
+
# Validate and convert line numbers
|
84
|
+
line_start = int(line_start)
|
85
|
+
line_end = int(line_end)
|
86
|
+
|
87
|
+
if line_start <= 0 or line_end <= 0:
|
88
|
+
raise ValueError("Line numbers must be positive integers")
|
89
|
+
if line_start > line_end:
|
90
|
+
raise ValueError("line_start must be less than or equal to line_end")
|
91
|
+
|
92
|
+
# Handle path expansion and normalization
|
93
|
+
file_path = os.path.expanduser(file_path)
|
94
|
+
file_path = os.path.abspath(file_path)
|
95
|
+
|
96
|
+
# Validate file exists and is readable
|
97
|
+
if not os.path.exists(file_path):
|
98
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
99
|
+
if not os.access(file_path, os.R_OK):
|
100
|
+
raise PermissionError(f"Permission denied reading file: {file_path}")
|
101
|
+
|
102
|
+
# Read file with explicit encoding and error handling
|
103
|
+
with open(file_path, encoding="utf-8", errors="strict") as f:
|
104
|
+
lines = f.readlines()
|
105
|
+
|
106
|
+
# Validate line numbers against file length
|
107
|
+
if line_start > len(lines):
|
108
|
+
raise ValueError(f"line_start {line_start} exceeds file length {len(lines)}")
|
109
|
+
|
110
|
+
# Calculate actual end line respecting MAX_LINES and file bounds
|
111
|
+
actual_end = min(line_end, line_start + MAX_LINES - 1, len(lines))
|
112
|
+
|
113
|
+
# Extract the block of lines
|
114
|
+
block = lines[line_start - 1 : actual_end]
|
115
|
+
|
116
|
+
# Determine if this is the last block of the file
|
117
|
+
is_last_block = actual_end == len(lines)
|
118
|
+
|
119
|
+
# Format result with clear boundaries and metadata
|
120
|
+
result = [
|
121
|
+
f"==== File: {file_path} ====",
|
122
|
+
f"==== Lines: {line_start}-{actual_end} of {len(lines)} ====",
|
123
|
+
"==== Content ====",
|
124
|
+
"".join(block).rstrip(),
|
125
|
+
"==== End of Block ====" + (" [LAST BLOCK SUCCESSFULLY READ]" if is_last_block else ""),
|
126
|
+
]
|
127
|
+
|
128
|
+
return "\n".join(result)
|
129
|
+
|
130
|
+
except (TypeError, ValueError) as e:
|
131
|
+
raise ValueError(f"Invalid line numbers: {e}")
|
132
|
+
except UnicodeDecodeError:
|
133
|
+
raise ValueError("File contains invalid UTF-8 characters")
|
134
|
+
except Exception as e:
|
135
|
+
raise RuntimeError(f"Error reading file: {e}")
|
136
|
+
|
137
|
+
|
138
|
+
if __name__ == "__main__":
|
139
|
+
tool = ReadFileBlockTool()
|
140
|
+
print(tool.to_markdown())
|
@@ -0,0 +1,79 @@
|
|
1
|
+
"""Tool for reading a file or HTTP content and returning its content."""
|
2
|
+
|
3
|
+
from urllib.parse import urlparse
|
4
|
+
|
5
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
6
|
+
from quantalogic.utils.read_file import read_file
|
7
|
+
from quantalogic.utils.read_http_text_content import read_http_text_content
|
8
|
+
|
9
|
+
MAX_LINES = 3000
|
10
|
+
|
11
|
+
|
12
|
+
class ReadFileTool(Tool):
|
13
|
+
"""Tool for reading a file or HTTP content and returning its content."""
|
14
|
+
|
15
|
+
name: str = "read_file_tool"
|
16
|
+
description: str = (
|
17
|
+
f"Reads a local file or HTTP content and returns its content."
|
18
|
+
f"Cut to {MAX_LINES} first lines.\n"
|
19
|
+
"Don't use on HTML files and large files."
|
20
|
+
"Prefer to use read file block tool to don't fill the memory."
|
21
|
+
)
|
22
|
+
arguments: list = [
|
23
|
+
ToolArgument(
|
24
|
+
name="file_path",
|
25
|
+
arg_type="string",
|
26
|
+
description="The path to the file or URL to read.",
|
27
|
+
required=True,
|
28
|
+
example="/path/to/file.txt or https://example.com/data.txt",
|
29
|
+
),
|
30
|
+
]
|
31
|
+
|
32
|
+
def _is_url(self, path: str) -> bool:
|
33
|
+
"""Check if the given path is a valid URL."""
|
34
|
+
try:
|
35
|
+
result = urlparse(path)
|
36
|
+
return all([result.scheme, result.netloc])
|
37
|
+
except ValueError:
|
38
|
+
return False
|
39
|
+
|
40
|
+
def _truncate_content(self, content: str) -> str:
|
41
|
+
"""Truncate the content to the first MAX_LINES lines."""
|
42
|
+
lines = content.splitlines()
|
43
|
+
truncated_lines = lines[:MAX_LINES]
|
44
|
+
truncated_content = "\n".join(truncated_lines)
|
45
|
+
if len(lines) > MAX_LINES:
|
46
|
+
truncated_content += f"\n\n[The content is too long. Truncated at {MAX_LINES} lines.]"
|
47
|
+
return truncated_content
|
48
|
+
|
49
|
+
def execute(self, file_path: str) -> str:
|
50
|
+
"""Reads a file or HTTP content and returns its content.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
file_path (str): The path to the file or URL to read.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
str: The content of the file or HTTP content.
|
57
|
+
"""
|
58
|
+
if self._is_url(file_path):
|
59
|
+
# Handle HTTP content
|
60
|
+
content, error = read_http_text_content(file_path)
|
61
|
+
if error:
|
62
|
+
return f"Error reading URL {file_path}: {error}"
|
63
|
+
truncated_content = self._truncate_content(content)
|
64
|
+
result = f"{truncated_content}"
|
65
|
+
return result
|
66
|
+
else:
|
67
|
+
# Handle local file
|
68
|
+
try:
|
69
|
+
content = read_file(file_path)
|
70
|
+
truncated_content = self._truncate_content(content)
|
71
|
+
result = f"{truncated_content}"
|
72
|
+
return result
|
73
|
+
except Exception as e:
|
74
|
+
return f"Error reading file {file_path}: {str(e)}"
|
75
|
+
|
76
|
+
|
77
|
+
if __name__ == "__main__":
|
78
|
+
tool = ReadFileTool()
|
79
|
+
print(tool.to_markdown())
|