quantalogic 0.2.27__py3-none-any.whl → 0.2.29__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/tools/execute_bash_command_tool.py +70 -53
- quantalogic/tools/replace_in_file_tool.py +23 -13
- {quantalogic-0.2.27.dist-info → quantalogic-0.2.29.dist-info}/METADATA +3 -1
- {quantalogic-0.2.27.dist-info → quantalogic-0.2.29.dist-info}/RECORD +7 -7
- {quantalogic-0.2.27.dist-info → quantalogic-0.2.29.dist-info}/LICENSE +0 -0
- {quantalogic-0.2.27.dist-info → quantalogic-0.2.29.dist-info}/WHEEL +0 -0
- {quantalogic-0.2.27.dist-info → quantalogic-0.2.29.dist-info}/entry_points.txt +0 -0
@@ -1,14 +1,18 @@
|
|
1
|
-
"""Tool for executing bash commands
|
1
|
+
"""Tool for executing bash commands with interactive input support."""
|
2
2
|
|
3
3
|
import os
|
4
|
+
import pty
|
5
|
+
import select
|
6
|
+
import signal
|
4
7
|
import subprocess
|
8
|
+
import sys
|
5
9
|
from typing import Dict, Optional, Union
|
6
10
|
|
7
11
|
from quantalogic.tools.tool import Tool, ToolArgument
|
8
12
|
|
9
13
|
|
10
14
|
class ExecuteBashCommandTool(Tool):
|
11
|
-
"""Tool for executing bash commands
|
15
|
+
"""Tool for executing bash commands with real-time I/O handling."""
|
12
16
|
|
13
17
|
name: str = "execute_bash_tool"
|
14
18
|
description: str = "Executes a bash command and returns its output."
|
@@ -35,7 +39,6 @@ class ExecuteBashCommandTool(Tool):
|
|
35
39
|
required=False,
|
36
40
|
example="60",
|
37
41
|
),
|
38
|
-
# Removed the `env` argument from ToolArgument since it doesn't support `dict` type
|
39
42
|
]
|
40
43
|
|
41
44
|
def execute(
|
@@ -45,72 +48,86 @@ class ExecuteBashCommandTool(Tool):
|
|
45
48
|
timeout: Union[int, str, None] = 60,
|
46
49
|
env: Optional[Dict[str, str]] = None,
|
47
50
|
) -> str:
|
48
|
-
"""Executes a bash command
|
49
|
-
|
50
|
-
|
51
|
-
command (str): The bash command to execute.
|
52
|
-
working_dir (str, optional): Working directory for command execution. Defaults to the current directory.
|
53
|
-
timeout (int or str, optional): Maximum execution time in seconds. Defaults to 60 seconds.
|
54
|
-
env (dict, optional): Environment variables to set for the command execution. Defaults to the current environment.
|
55
|
-
|
56
|
-
Returns:
|
57
|
-
str: The command output or error message.
|
58
|
-
|
59
|
-
Raises:
|
60
|
-
subprocess.TimeoutExpired: If the command execution exceeds the timeout.
|
61
|
-
subprocess.CalledProcessError: If the command returns a non-zero exit status.
|
62
|
-
ValueError: If the timeout cannot be converted to an integer.
|
63
|
-
"""
|
64
|
-
# Convert timeout to integer, defaulting to 60 if None or invalid
|
65
|
-
try:
|
66
|
-
timeout_seconds = int(timeout) if timeout else 60
|
67
|
-
except (ValueError, TypeError):
|
68
|
-
timeout_seconds = 60
|
69
|
-
|
70
|
-
# Use the current working directory if no working directory is specified
|
71
|
-
cwd = working_dir if working_dir else os.getcwd()
|
72
|
-
|
73
|
-
# Use the current environment if no custom environment is specified
|
51
|
+
"""Executes a bash command with interactive input handling."""
|
52
|
+
timeout_seconds = int(timeout) if timeout else 60
|
53
|
+
cwd = working_dir or os.getcwd()
|
74
54
|
env_vars = os.environ.copy()
|
75
55
|
if env:
|
76
56
|
env_vars.update(env)
|
77
57
|
|
78
58
|
try:
|
79
|
-
|
80
|
-
|
59
|
+
master, slave = pty.openpty()
|
60
|
+
proc = subprocess.Popen(
|
81
61
|
command,
|
82
62
|
shell=True,
|
63
|
+
stdin=slave,
|
64
|
+
stdout=slave,
|
65
|
+
stderr=subprocess.STDOUT,
|
83
66
|
cwd=cwd,
|
84
|
-
capture_output=True,
|
85
|
-
text=True,
|
86
|
-
timeout=timeout_seconds,
|
87
67
|
env=env_vars,
|
68
|
+
preexec_fn=os.setsid,
|
69
|
+
close_fds=True,
|
88
70
|
)
|
89
|
-
|
90
|
-
|
71
|
+
os.close(slave)
|
72
|
+
|
73
|
+
stdout_buffer = []
|
74
|
+
break_loop = False
|
75
|
+
|
76
|
+
try:
|
77
|
+
while True:
|
78
|
+
rlist, _, _ = select.select([master, sys.stdin], [], [], timeout_seconds)
|
79
|
+
if not rlist:
|
80
|
+
if proc.poll() is not None:
|
81
|
+
break # Process completed but select timed out
|
82
|
+
raise subprocess.TimeoutExpired(command, timeout_seconds)
|
83
|
+
|
84
|
+
for fd in rlist:
|
85
|
+
if fd == master:
|
86
|
+
data = os.read(master, 1024).decode()
|
87
|
+
if not data:
|
88
|
+
break_loop = True
|
89
|
+
break
|
90
|
+
stdout_buffer.append(data)
|
91
|
+
sys.stdout.write(data)
|
92
|
+
sys.stdout.flush()
|
93
|
+
elif fd == sys.stdin:
|
94
|
+
user_input = os.read(sys.stdin.fileno(), 1024)
|
95
|
+
os.write(master, user_input)
|
96
|
+
|
97
|
+
# Check if process completed or EOF received
|
98
|
+
if break_loop or proc.poll() is not None:
|
99
|
+
# Read any remaining output
|
100
|
+
while True:
|
101
|
+
data = os.read(master, 1024).decode()
|
102
|
+
if not data:
|
103
|
+
break
|
104
|
+
stdout_buffer.append(data)
|
105
|
+
sys.stdout.write(data)
|
106
|
+
sys.stdout.flush()
|
107
|
+
break
|
108
|
+
|
109
|
+
except subprocess.TimeoutExpired:
|
110
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
111
|
+
return f"Command timed out after {timeout_seconds} seconds."
|
112
|
+
except EOFError:
|
113
|
+
pass # Process exited normally
|
114
|
+
finally:
|
115
|
+
os.close(master)
|
116
|
+
proc.wait()
|
117
|
+
|
118
|
+
stdout_content = ''.join(stdout_buffer)
|
119
|
+
return_code = proc.returncode
|
120
|
+
formatted_result = (
|
91
121
|
"<command_output>"
|
92
|
-
f" <stdout>"
|
93
|
-
f"{
|
94
|
-
f" </stdout>"
|
95
|
-
f" <stderr>"
|
96
|
-
f"{result.stderr.strip()}"
|
97
|
-
f" </stderr>"
|
98
|
-
f" <returncode>"
|
99
|
-
f" {result.returncode}"
|
100
|
-
f" </returncode>"
|
122
|
+
f" <stdout>{stdout_content.strip()}</stdout>"
|
123
|
+
f" <returncode>{return_code}</returncode>"
|
101
124
|
f"</command_output>"
|
102
125
|
)
|
103
|
-
|
104
|
-
return formated_result
|
105
|
-
|
106
|
-
except subprocess.TimeoutExpired:
|
107
|
-
return f"Command timed out after {timeout_seconds} seconds."
|
108
|
-
except subprocess.CalledProcessError as e:
|
109
|
-
return f"Command failed with error: {e.stderr.strip()}"
|
126
|
+
return formatted_result
|
110
127
|
except Exception as e:
|
111
128
|
return f"Unexpected error executing command: {str(e)}"
|
112
129
|
|
113
130
|
|
114
131
|
if __name__ == "__main__":
|
115
132
|
tool = ExecuteBashCommandTool()
|
116
|
-
print(tool.to_markdown())
|
133
|
+
print(tool.to_markdown())
|
@@ -65,7 +65,6 @@ class ReplaceInFileTool(Tool):
|
|
65
65
|
)
|
66
66
|
need_validation: bool = True
|
67
67
|
|
68
|
-
# Adjust this threshold to allow more or less approximate matching
|
69
68
|
SIMILARITY_THRESHOLD: float = 0.85
|
70
69
|
|
71
70
|
arguments: list[ToolArgument] = [
|
@@ -124,6 +123,15 @@ class ReplaceInFileTool(Tool):
|
|
124
123
|
),
|
125
124
|
]
|
126
125
|
|
126
|
+
def normalize_whitespace(self, text: str) -> str:
|
127
|
+
"""Normalize leading whitespace by converting tabs to spaces."""
|
128
|
+
return '\n'.join([self._normalize_line(line) for line in text.split('\n')])
|
129
|
+
|
130
|
+
def _normalize_line(self, line: str) -> str:
|
131
|
+
"""Normalize leading whitespace in a single line."""
|
132
|
+
leading_ws = len(line) - len(line.lstrip())
|
133
|
+
return line.replace('\t', ' ', leading_ws) # Convert tabs to 4 spaces only in leading whitespace
|
134
|
+
|
127
135
|
def parse_diff(self, diff: str) -> list[SearchReplaceBlock]:
|
128
136
|
"""Parses the diff string into a list of SearchReplaceBlock instances."""
|
129
137
|
if not diff or not diff.strip():
|
@@ -250,6 +258,7 @@ class ReplaceInFileTool(Tool):
|
|
250
258
|
except Exception as e:
|
251
259
|
return f"Error: Failed to write changes to '{path}': {str(e) or 'Unknown error'}"
|
252
260
|
|
261
|
+
# Maintain original success message format
|
253
262
|
message = [f"Successfully modified '{path}'"]
|
254
263
|
for idx, block in enumerate(blocks, 1):
|
255
264
|
status = "Exact match" if block.similarity is None else f"Similar match ({block.similarity:.1%})"
|
@@ -267,26 +276,27 @@ class ReplaceInFileTool(Tool):
|
|
267
276
|
return f"Error: Unexpected error occurred - {error_msg or 'Unknown error'}"
|
268
277
|
|
269
278
|
def find_similar_match(self, search: str, content: str) -> Tuple[float, str]:
|
270
|
-
"""Finds the most similar substring in content compared to search."""
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
content_lines = content.splitlines()
|
279
|
+
"""Finds the most similar substring in content compared to search with whitespace normalization."""
|
280
|
+
norm_search = self.normalize_whitespace(search)
|
281
|
+
content_lines = content.split('\n')
|
282
|
+
norm_content = self.normalize_whitespace(content)
|
283
|
+
norm_content_lines = norm_content.split('\n')
|
276
284
|
|
277
|
-
if len(
|
285
|
+
if len(norm_content_lines) < len(norm_search.split('\n')):
|
278
286
|
return 0.0, ""
|
279
287
|
|
280
288
|
max_similarity = 0.0
|
281
289
|
best_match = ""
|
290
|
+
search_line_count = len(norm_search.split('\n'))
|
282
291
|
|
283
|
-
for i in range(len(
|
284
|
-
|
285
|
-
similarity = difflib.SequenceMatcher(None,
|
292
|
+
for i in range(len(norm_content_lines) - search_line_count + 1):
|
293
|
+
candidate_norm = '\n'.join(norm_content_lines[i:i+search_line_count])
|
294
|
+
similarity = difflib.SequenceMatcher(None, norm_search, candidate_norm).ratio()
|
286
295
|
|
287
296
|
if similarity > max_similarity:
|
288
297
|
max_similarity = similarity
|
289
|
-
|
298
|
+
# Get original lines (non-normalized) for accurate replacement
|
299
|
+
best_match = '\n'.join(content_lines[i:i+search_line_count])
|
290
300
|
|
291
301
|
return max_similarity, best_match
|
292
302
|
|
@@ -297,4 +307,4 @@ class ReplaceInFileTool(Tool):
|
|
297
307
|
|
298
308
|
if __name__ == "__main__":
|
299
309
|
tool = ReplaceInFileTool()
|
300
|
-
print(tool.to_markdown())
|
310
|
+
print(tool.to_markdown())
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: quantalogic
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.29
|
4
4
|
Summary: QuantaLogic ReAct Agents
|
5
5
|
Author: Raphaël MANSUY
|
6
6
|
Author-email: raphael.mansuy@gmail.com
|
@@ -63,8 +63,10 @@ Description-Content-Type: text/markdown
|
|
63
63
|
[](https://quantalogic.github.io/quantalogic/)
|
64
64
|
|
65
65
|
|
66
|
+
|
66
67
|
QuantaLogic is a ReAct (Reasoning & Action) framework for building advanced AI agents.
|
67
68
|
|
69
|
+
|
68
70
|
It seamlessly integrates large language models (LLMs) with a robust tool system, enabling agents to understand, reason about, and execute complex tasks through natural language interaction.
|
69
71
|
|
70
72
|
The `cli` version include coding capabilities comparable to Aider.
|
@@ -33,7 +33,7 @@ quantalogic/tools/download_http_file_tool.py,sha256=wTfanbXjIRi5-qrbluuLvNmDNhvm
|
|
33
33
|
quantalogic/tools/duckduckgo_search_tool.py,sha256=xVaEb_SUK5NL3lwMQXj1rGQYYvNT-td-qaB9QCes27Q,7014
|
34
34
|
quantalogic/tools/edit_whole_content_tool.py,sha256=nXmpAvojvqvAcqNMy1kUKZ1ocboky_ZcnCR4SNCSPgw,2360
|
35
35
|
quantalogic/tools/elixir_tool.py,sha256=fzPPtAW-Koy9KB0r5k2zV1f1U0WphL-LXPPOBkeNkug,7652
|
36
|
-
quantalogic/tools/execute_bash_command_tool.py,sha256=
|
36
|
+
quantalogic/tools/execute_bash_command_tool.py,sha256=0JGeJobY1QC6mx8HZYTqNAUg5cNc5Xn8a26J45XaDRE,4693
|
37
37
|
quantalogic/tools/generate_database_report_tool.py,sha256=QbZjtmegGEOEZAIa-CSeBo5O9dYBZTk_PWrumyFUg1Q,1890
|
38
38
|
quantalogic/tools/grep_app_tool.py,sha256=BDxygwx7WCbqbiP2jmSRnIsoIUVYG5A4SKzId524ys4,19957
|
39
39
|
quantalogic/tools/input_question_tool.py,sha256=UoTlNhdmdr-eyiVtVCG2qJe_R4bU_ag-DzstSdmYkvM,1848
|
@@ -57,7 +57,7 @@ quantalogic/tools/python_tool.py,sha256=70HLbfU2clOBgj4axDOtIKzXwEBMNGEAX1nGSf-K
|
|
57
57
|
quantalogic/tools/read_file_block_tool.py,sha256=FTcDAUOOPQOvWRjnRI6nMI1Upus90klR4PC0pbPP_S8,5266
|
58
58
|
quantalogic/tools/read_file_tool.py,sha256=l6k-SOIV9krpXAmUTkxzua51S-KHgzGqkcDlD5AD8K0,2710
|
59
59
|
quantalogic/tools/read_html_tool.py,sha256=Vq2rHY8a36z1-4rN6c_kYjPUTQ4I2UT154PMpaoWSkA,11139
|
60
|
-
quantalogic/tools/replace_in_file_tool.py,sha256=
|
60
|
+
quantalogic/tools/replace_in_file_tool.py,sha256=AM2XSF5WMI48gOyAGsnUOeW-jlyorWV182Yw_BA26_o,13719
|
61
61
|
quantalogic/tools/ripgrep_tool.py,sha256=sRzHaWac9fa0cCGhECJN04jw_Ko0O3u45KDWzMIYcvY,14291
|
62
62
|
quantalogic/tools/search_definition_names.py,sha256=Qj9ex226vHs8Jf-kydmTh7B_R8O5buIsJpQu3CvYw7k,18601
|
63
63
|
quantalogic/tools/serpapi_search_tool.py,sha256=sX-Noch77kGP2XiwislPNFyy3_4TH6TwMK6C81L3q9Y,5316
|
@@ -85,8 +85,8 @@ quantalogic/version_check.py,sha256=cttR1lR3OienGLl7NrK1Te1fhDkqSjCci7HC1vFUTSY,
|
|
85
85
|
quantalogic/welcome_message.py,sha256=IXMhem8h7srzNUwvw8G_lmEkHU8PFfote021E_BXmVk,3039
|
86
86
|
quantalogic/xml_parser.py,sha256=uMLQNHTRCg116FwcjRoquZmSwVtE4LEH-6V2E3RD-dA,11466
|
87
87
|
quantalogic/xml_tool_parser.py,sha256=Vz4LEgDbelJynD1siLOVkJ3gLlfHsUk65_gCwbYJyGc,3784
|
88
|
-
quantalogic-0.2.
|
89
|
-
quantalogic-0.2.
|
90
|
-
quantalogic-0.2.
|
91
|
-
quantalogic-0.2.
|
92
|
-
quantalogic-0.2.
|
88
|
+
quantalogic-0.2.29.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
89
|
+
quantalogic-0.2.29.dist-info/METADATA,sha256=Nnd_JEcVXdVip81fApVj5t3pRLvF4boV1TXteHwKB5k,20534
|
90
|
+
quantalogic-0.2.29.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
91
|
+
quantalogic-0.2.29.dist-info/entry_points.txt,sha256=h74O_Q3qBRCrDR99qvwB4BpBGzASPUIjCfxHq6Qnups,183
|
92
|
+
quantalogic-0.2.29.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|