quantalogic 0.28__py3-none-any.whl → 0.30.1__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/main.py +70 -4
- quantalogic/tools/execute_bash_command_tool.py +80 -13
- quantalogic/tools/replace_in_file_tool.py +23 -13
- {quantalogic-0.28.dist-info → quantalogic-0.30.1.dist-info}/METADATA +7 -1
- {quantalogic-0.28.dist-info → quantalogic-0.30.1.dist-info}/RECORD +8 -8
- {quantalogic-0.28.dist-info → quantalogic-0.30.1.dist-info}/LICENSE +0 -0
- {quantalogic-0.28.dist-info → quantalogic-0.30.1.dist-info}/WHEEL +0 -0
- {quantalogic-0.28.dist-info → quantalogic-0.30.1.dist-info}/entry_points.txt +0 -0
quantalogic/main.py
CHANGED
@@ -7,10 +7,15 @@ from typing import Optional
|
|
7
7
|
|
8
8
|
# Third-party imports
|
9
9
|
import click
|
10
|
+
from dotenv import load_dotenv
|
10
11
|
from loguru import logger
|
11
12
|
|
12
13
|
from quantalogic.version import get_version
|
13
14
|
|
15
|
+
# Load environment variables from .env file
|
16
|
+
load_dotenv()
|
17
|
+
|
18
|
+
|
14
19
|
# Configure logger
|
15
20
|
logger.remove()
|
16
21
|
|
@@ -23,9 +28,52 @@ from quantalogic.agent_config import ( # noqa: E402
|
|
23
28
|
)
|
24
29
|
from quantalogic.task_runner import task_runner # noqa: E402
|
25
30
|
|
31
|
+
# Platform-specific imports
|
32
|
+
try:
|
33
|
+
if sys.platform == 'win32':
|
34
|
+
import msvcrt # Built-in Windows module
|
35
|
+
else:
|
36
|
+
import termios
|
37
|
+
import tty
|
38
|
+
except ImportError as e:
|
39
|
+
logger.warning(f"Could not import platform-specific module: {e}")
|
40
|
+
# Fall back to basic terminal handling if imports fail
|
41
|
+
msvcrt = None
|
42
|
+
termios = None
|
43
|
+
tty = None
|
44
|
+
|
26
45
|
AGENT_MODES = ["code", "basic", "interpreter", "full", "code-basic", "search", "search-full"]
|
27
46
|
|
28
47
|
|
48
|
+
def setup_terminal():
|
49
|
+
"""Configure terminal settings based on platform."""
|
50
|
+
if sys.platform == 'win32':
|
51
|
+
if msvcrt:
|
52
|
+
return None # Windows terminal is already configured
|
53
|
+
logger.warning("msvcrt module not available on Windows")
|
54
|
+
return None
|
55
|
+
else:
|
56
|
+
if termios and tty:
|
57
|
+
try:
|
58
|
+
fd = sys.stdin.fileno()
|
59
|
+
old_settings = termios.tcgetattr(fd)
|
60
|
+
tty.setraw(fd)
|
61
|
+
return old_settings
|
62
|
+
except (termios.error, AttributeError) as e:
|
63
|
+
logger.warning(f"Failed to configure terminal: {e}")
|
64
|
+
return None
|
65
|
+
return None
|
66
|
+
|
67
|
+
|
68
|
+
def restore_terminal(old_settings):
|
69
|
+
"""Restore terminal settings based on platform."""
|
70
|
+
if sys.platform != 'win32' and termios and old_settings:
|
71
|
+
try:
|
72
|
+
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_settings)
|
73
|
+
except (termios.error, AttributeError) as e:
|
74
|
+
logger.warning(f"Failed to restore terminal settings: {e}")
|
75
|
+
|
76
|
+
|
29
77
|
@click.group(invoke_without_command=True)
|
30
78
|
@click.option(
|
31
79
|
"--compact-every-n-iteration",
|
@@ -37,7 +85,14 @@ AGENT_MODES = ["code", "basic", "interpreter", "full", "code-basic", "search", "
|
|
37
85
|
@click.option(
|
38
86
|
"--model-name",
|
39
87
|
default=MODEL_NAME,
|
40
|
-
help='Specify the model to use (litellm format
|
88
|
+
help='Specify the model to use (litellm format). Examples:\n'
|
89
|
+
' - openai/gpt-4o-mini\n'
|
90
|
+
' - openai/gpt-4o\n'
|
91
|
+
' - anthropic/claude-3.5-sonnet\n'
|
92
|
+
' - deepseek/deepseek-chat\n'
|
93
|
+
' - deepseek/deepseek-reasoner\n'
|
94
|
+
' - openrouter/deepseek/deepseek-r1\n'
|
95
|
+
' - openrouter/openai/gpt-4o',
|
41
96
|
)
|
42
97
|
@click.option(
|
43
98
|
"--log",
|
@@ -77,7 +132,14 @@ def cli(
|
|
77
132
|
compact_every_n_iteration: int | None,
|
78
133
|
max_tokens_working_memory: int | None,
|
79
134
|
) -> None:
|
80
|
-
"""QuantaLogic AI Assistant - A powerful AI tool for various tasks.
|
135
|
+
"""QuantaLogic AI Assistant - A powerful AI tool for various tasks.
|
136
|
+
|
137
|
+
Environment Variables:
|
138
|
+
- OpenAI: Set `OPENAI_API_KEY` to your OpenAI API key.
|
139
|
+
- Anthropic: Set `ANTHROPIC_API_KEY` to your Anthropic API key.
|
140
|
+
- DeepSeek: Set `DEEPSEEK_API_KEY` to your DeepSeek API key.
|
141
|
+
Use a `.env` file or export these variables in your shell for seamless integration.
|
142
|
+
"""
|
81
143
|
if version:
|
82
144
|
console = Console()
|
83
145
|
current_version = get_version()
|
@@ -181,8 +243,12 @@ def task(
|
|
181
243
|
|
182
244
|
|
183
245
|
def main():
|
184
|
-
"""Main
|
185
|
-
|
246
|
+
"""Main entry point."""
|
247
|
+
old_settings = setup_terminal()
|
248
|
+
try:
|
249
|
+
cli() # type: ignore
|
250
|
+
finally:
|
251
|
+
restore_terminal(old_settings)
|
186
252
|
|
187
253
|
|
188
254
|
if __name__ == "__main__":
|
@@ -1,13 +1,22 @@
|
|
1
1
|
"""Tool for executing bash commands with interactive input support."""
|
2
2
|
|
3
3
|
import os
|
4
|
-
import pty
|
5
4
|
import select
|
6
5
|
import signal
|
7
6
|
import subprocess
|
8
7
|
import sys
|
9
8
|
from typing import Dict, Optional, Union
|
10
9
|
|
10
|
+
from loguru import logger
|
11
|
+
|
12
|
+
# Platform-specific imports
|
13
|
+
try:
|
14
|
+
if sys.platform != 'win32':
|
15
|
+
import pty
|
16
|
+
except ImportError as e:
|
17
|
+
logger.warning(f"Could not import platform-specific module: {e}")
|
18
|
+
pty = None
|
19
|
+
|
11
20
|
from quantalogic.tools.tool import Tool, ToolArgument
|
12
21
|
|
13
22
|
|
@@ -41,20 +50,58 @@ class ExecuteBashCommandTool(Tool):
|
|
41
50
|
),
|
42
51
|
]
|
43
52
|
|
44
|
-
def
|
53
|
+
def _execute_windows(
|
45
54
|
self,
|
46
55
|
command: str,
|
47
|
-
|
48
|
-
|
49
|
-
|
56
|
+
cwd: str,
|
57
|
+
timeout_seconds: int,
|
58
|
+
env_vars: Dict[str, str],
|
50
59
|
) -> str:
|
51
|
-
"""
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
60
|
+
"""Execute command on Windows platform."""
|
61
|
+
try:
|
62
|
+
# On Windows, use subprocess with pipes
|
63
|
+
process = subprocess.Popen(
|
64
|
+
command,
|
65
|
+
shell=True,
|
66
|
+
stdin=subprocess.PIPE,
|
67
|
+
stdout=subprocess.PIPE,
|
68
|
+
stderr=subprocess.PIPE,
|
69
|
+
cwd=cwd,
|
70
|
+
env=env_vars,
|
71
|
+
text=True,
|
72
|
+
encoding='utf-8'
|
73
|
+
)
|
74
|
+
|
75
|
+
try:
|
76
|
+
stdout, stderr = process.communicate(timeout=timeout_seconds)
|
77
|
+
return_code = process.returncode
|
78
|
+
|
79
|
+
if return_code != 0 and stderr:
|
80
|
+
logger.warning(f"Command failed with error: {stderr}")
|
81
|
+
|
82
|
+
formatted_result = (
|
83
|
+
"<command_output>"
|
84
|
+
f" <stdout>{stdout.strip()}</stdout>"
|
85
|
+
f" <returncode>{return_code}</returncode>"
|
86
|
+
f"</command_output>"
|
87
|
+
)
|
88
|
+
return formatted_result
|
89
|
+
|
90
|
+
except subprocess.TimeoutExpired:
|
91
|
+
process.kill()
|
92
|
+
return f"Command timed out after {timeout_seconds} seconds."
|
93
|
+
|
94
|
+
except Exception as e:
|
95
|
+
return f"Unexpected error executing command: {str(e)}"
|
57
96
|
|
97
|
+
def _execute_unix(
|
98
|
+
self,
|
99
|
+
command: str,
|
100
|
+
cwd: str,
|
101
|
+
timeout_seconds: int,
|
102
|
+
env_vars: Dict[str, str],
|
103
|
+
) -> str:
|
104
|
+
"""Execute command on Unix platform."""
|
58
105
|
try:
|
59
106
|
master, slave = pty.openpty()
|
60
107
|
proc = subprocess.Popen(
|
@@ -94,9 +141,7 @@ class ExecuteBashCommandTool(Tool):
|
|
94
141
|
user_input = os.read(sys.stdin.fileno(), 1024)
|
95
142
|
os.write(master, user_input)
|
96
143
|
|
97
|
-
# Check if process completed or EOF received
|
98
144
|
if break_loop or proc.poll() is not None:
|
99
|
-
# Read any remaining output
|
100
145
|
while True:
|
101
146
|
data = os.read(master, 1024).decode()
|
102
147
|
if not data:
|
@@ -127,6 +172,28 @@ class ExecuteBashCommandTool(Tool):
|
|
127
172
|
except Exception as e:
|
128
173
|
return f"Unexpected error executing command: {str(e)}"
|
129
174
|
|
175
|
+
def execute(
|
176
|
+
self,
|
177
|
+
command: str,
|
178
|
+
working_dir: Optional[str] = None,
|
179
|
+
timeout: Union[int, str, None] = 60,
|
180
|
+
env: Optional[Dict[str, str]] = None,
|
181
|
+
) -> str:
|
182
|
+
"""Executes a bash command with interactive input handling."""
|
183
|
+
timeout_seconds = int(timeout) if timeout else 60
|
184
|
+
cwd = working_dir or os.getcwd()
|
185
|
+
env_vars = os.environ.copy()
|
186
|
+
if env:
|
187
|
+
env_vars.update(env)
|
188
|
+
|
189
|
+
if sys.platform == 'win32':
|
190
|
+
return self._execute_windows(command, cwd, timeout_seconds, env_vars)
|
191
|
+
else:
|
192
|
+
if not pty:
|
193
|
+
logger.warning("PTY module not available, falling back to Windows-style execution")
|
194
|
+
return self._execute_windows(command, cwd, timeout_seconds, env_vars)
|
195
|
+
return self._execute_unix(command, cwd, timeout_seconds, env_vars)
|
196
|
+
|
130
197
|
|
131
198
|
if __name__ == "__main__":
|
132
199
|
tool = ExecuteBashCommandTool()
|
@@ -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.
|
3
|
+
Version: 0.30.1
|
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.
|
@@ -92,6 +94,10 @@ We created [QuantaLogic](https://www.quantalogic.app) because we saw a significa
|
|
92
94
|
- **Memory Management**: Intelligent context handling and optimization
|
93
95
|
- **Enterprise Ready**: Comprehensive logging, error handling, and validation system
|
94
96
|
|
97
|
+
## Environment Configuration
|
98
|
+
|
99
|
+
To configure the environment API key for Quantalogic using LiteLLM, set the required environment variable for your chosen provider (e.g., `OPENAI_API_KEY` for OpenAI, `ANTHROPIC_API_KEY` for Anthropic, or `DEEPSEEK_API_KEY` for DeepSeek) and any optional variables like `OPENAI_API_BASE` or `OPENROUTER_REFERRER`. Use a `.env` file or a secrets manager to securely store these keys, and load them in your code using `python-dotenv`. For advanced configurations, refer to the [LiteLLM documentation](https://docs.litellm.ai/docs/).
|
100
|
+
|
95
101
|
## 📋 Table of Contents
|
96
102
|
|
97
103
|
- [Release Notes](#release-notes)
|
@@ -10,7 +10,7 @@ quantalogic/event_emitter.py,sha256=jqot2g4JRXc88K6PW837Oqxbf7shZfO-xdPaUWmzupk,
|
|
10
10
|
quantalogic/generative_model.py,sha256=az_kqWdEBQRROvvYZu6d68JQ4nzDbQG23gADeHLMypc,16761
|
11
11
|
quantalogic/get_model_info.py,sha256=YCBZ8qynlq_iLUc--xBrQxacFZL9RHZPv5cdVwjukcw,602
|
12
12
|
quantalogic/interactive_text_editor.py,sha256=_pNPnUG3Y3_YX0R9-kx0vcaUWU0AAC350jpJ5UjrTuE,6986
|
13
|
-
quantalogic/main.py,sha256=
|
13
|
+
quantalogic/main.py,sha256=rxhXMkvFj36fP7Gbl3o1ub_aiXK1yl5YlVySI-ldTQI,7430
|
14
14
|
quantalogic/memory.py,sha256=zbtRuM05jaS2lJll-92dt5JfYVLERnF_m_9xqp2x-k0,6304
|
15
15
|
quantalogic/model_names.py,sha256=UZlz25zG9B2dpfwdw_e1Gw5qFsKQ7iME9FJh9Ts4u6s,938
|
16
16
|
quantalogic/prompts.py,sha256=CW4CRgW1hTpXeWdeJNbPaRPUeUm-xKuGHJrT8mOtvkw,3602
|
@@ -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=kl3RSOZCOc-U52dwd0h6BxXvjMlAX7D0Bo2-HkCOcxo,6908
|
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.
|
89
|
-
quantalogic-0.
|
90
|
-
quantalogic-0.
|
91
|
-
quantalogic-0.
|
92
|
-
quantalogic-0.
|
88
|
+
quantalogic-0.30.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
89
|
+
quantalogic-0.30.1.dist-info/METADATA,sha256=4JtCY2xklibv4XLqIIaA4jKtxs1GPbDKiO8Zj_Uk32o,21096
|
90
|
+
quantalogic-0.30.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
91
|
+
quantalogic-0.30.1.dist-info/entry_points.txt,sha256=h74O_Q3qBRCrDR99qvwB4BpBGzASPUIjCfxHq6Qnups,183
|
92
|
+
quantalogic-0.30.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|