janito 1.5.2__py3-none-any.whl → 1.6.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.
- janito/__init__.py +1 -1
- janito/__main__.py +0 -1
- janito/agent/config.py +11 -10
- janito/agent/config_defaults.py +3 -2
- janito/agent/conversation.py +93 -119
- janito/agent/conversation_api.py +98 -0
- janito/agent/conversation_exceptions.py +12 -0
- janito/agent/conversation_tool_calls.py +22 -0
- janito/agent/conversation_ui.py +17 -0
- janito/agent/message_handler.py +8 -9
- janito/agent/{agent.py → openai_client.py} +48 -16
- janito/agent/openai_schema_generator.py +53 -37
- janito/agent/profile_manager.py +172 -0
- janito/agent/queued_message_handler.py +13 -14
- janito/agent/rich_live.py +32 -0
- janito/agent/rich_message_handler.py +64 -0
- janito/agent/runtime_config.py +6 -1
- janito/agent/{tools/tool_base.py → tool_base.py} +15 -8
- janito/agent/tool_registry.py +118 -132
- janito/agent/tools/__init__.py +41 -2
- janito/agent/tools/ask_user.py +43 -33
- janito/agent/tools/create_directory.py +18 -16
- janito/agent/tools/create_file.py +31 -36
- janito/agent/tools/fetch_url.py +23 -19
- janito/agent/tools/find_files.py +40 -36
- janito/agent/tools/get_file_outline.py +100 -22
- janito/agent/tools/get_lines.py +40 -32
- janito/agent/tools/gitignore_utils.py +9 -6
- janito/agent/tools/move_file.py +22 -13
- janito/agent/tools/py_compile_file.py +40 -0
- janito/agent/tools/remove_directory.py +34 -24
- janito/agent/tools/remove_file.py +22 -20
- janito/agent/tools/replace_file.py +51 -0
- janito/agent/tools/replace_text_in_file.py +69 -42
- janito/agent/tools/rich_live.py +9 -2
- janito/agent/tools/run_bash_command.py +155 -107
- janito/agent/tools/run_python_command.py +139 -0
- janito/agent/tools/search_files.py +51 -34
- janito/agent/tools/tools_utils.py +4 -2
- janito/agent/tools/utils.py +6 -2
- janito/cli/_print_config.py +42 -16
- janito/cli/_utils.py +1 -0
- janito/cli/arg_parser.py +182 -29
- janito/cli/config_commands.py +54 -22
- janito/cli/logging_setup.py +9 -3
- janito/cli/main.py +11 -10
- janito/cli/runner/__init__.py +2 -0
- janito/cli/runner/cli_main.py +148 -0
- janito/cli/runner/config.py +33 -0
- janito/cli/runner/formatting.py +12 -0
- janito/cli/runner/scan.py +44 -0
- janito/cli_chat_shell/__init__.py +0 -1
- janito/cli_chat_shell/chat_loop.py +71 -92
- janito/cli_chat_shell/chat_state.py +38 -0
- janito/cli_chat_shell/chat_ui.py +43 -0
- janito/cli_chat_shell/commands/__init__.py +45 -0
- janito/cli_chat_shell/commands/config.py +22 -0
- janito/cli_chat_shell/commands/history_reset.py +29 -0
- janito/cli_chat_shell/commands/session.py +48 -0
- janito/cli_chat_shell/commands/session_control.py +12 -0
- janito/cli_chat_shell/commands/system.py +73 -0
- janito/cli_chat_shell/commands/utility.py +29 -0
- janito/cli_chat_shell/config_shell.py +39 -10
- janito/cli_chat_shell/load_prompt.py +5 -2
- janito/cli_chat_shell/session_manager.py +24 -27
- janito/cli_chat_shell/ui.py +75 -40
- janito/rich_utils.py +15 -2
- janito/web/__main__.py +10 -2
- janito/web/app.py +88 -52
- {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/METADATA +76 -11
- janito-1.6.0.dist-info/RECORD +81 -0
- {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/WHEEL +1 -1
- janito/agent/rich_tool_handler.py +0 -43
- janito/agent/templates/system_instructions.j2 +0 -38
- janito/agent/tool_auto_imports.py +0 -5
- janito/agent/tools/append_text_to_file.py +0 -41
- janito/agent/tools/py_compile.py +0 -39
- janito/agent/tools/python_exec.py +0 -83
- janito/cli/runner.py +0 -137
- janito/cli_chat_shell/commands.py +0 -204
- janito/render_prompt.py +0 -13
- janito-1.5.2.dist-info/RECORD +0 -66
- {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/entry_points.txt +0 -0
- {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
from janito.agent.tool_registry import register_tool
|
4
|
+
from janito.agent.tools.utils import expand_path, display_path
|
5
|
+
from janito.agent.tool_base import ToolBase
|
6
|
+
|
7
|
+
|
8
|
+
@register_tool(name="replace_file")
|
9
|
+
class ReplaceFileTool(ToolBase):
|
10
|
+
"""
|
11
|
+
Overwrite (replace) a file with the given content. Creates the file if it does not exist.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
path (str): Path to the file to overwrite or create.
|
15
|
+
content (str): Content to write to the file.
|
16
|
+
backup (bool, optional): If True, create a backup (.bak) before replacing if the file exists. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
|
17
|
+
Returns:
|
18
|
+
str: Status message indicating the result. Example:
|
19
|
+
- "\u2705 Successfully replaced the file at ..."
|
20
|
+
- "\u2705 Successfully created the file at ..."
|
21
|
+
"""
|
22
|
+
|
23
|
+
def call(self, path: str, content: str, backup: bool = False) -> str:
|
24
|
+
original_path = path
|
25
|
+
path = expand_path(path)
|
26
|
+
disp_path = display_path(original_path, path)
|
27
|
+
updating = os.path.exists(path) and not os.path.isdir(path)
|
28
|
+
if os.path.exists(path) and os.path.isdir(path):
|
29
|
+
self.report_error("\u274c Error: is a directory")
|
30
|
+
return (
|
31
|
+
f"\u274c Cannot replace file: '{disp_path}' is an existing directory."
|
32
|
+
)
|
33
|
+
# Ensure parent directories exist
|
34
|
+
dir_name = os.path.dirname(path)
|
35
|
+
if dir_name:
|
36
|
+
os.makedirs(dir_name, exist_ok=True)
|
37
|
+
if backup and os.path.exists(path) and not os.path.isdir(path):
|
38
|
+
shutil.copy2(path, path + ".bak")
|
39
|
+
with open(path, "w", encoding="utf-8", errors="replace") as f:
|
40
|
+
f.write(content)
|
41
|
+
new_lines = content.count("\n") + 1 if content else 0
|
42
|
+
if updating:
|
43
|
+
self.report_success(
|
44
|
+
f"\u2705 Successfully replaced the file at '{disp_path}' ({new_lines} lines)."
|
45
|
+
)
|
46
|
+
return f"\u2705 Successfully replaced the file at '{disp_path}' ({new_lines} lines)."
|
47
|
+
else:
|
48
|
+
self.report_success(
|
49
|
+
f"\u2705 Successfully created the file at '{disp_path}' ({new_lines} lines)."
|
50
|
+
)
|
51
|
+
return f"\u2705 Successfully created the file at '{disp_path}' ({new_lines} lines)."
|
@@ -1,46 +1,56 @@
|
|
1
|
-
from janito.agent.
|
1
|
+
from janito.agent.tool_base import ToolBase
|
2
2
|
from janito.agent.tool_registry import register_tool
|
3
|
-
|
4
|
-
@register_tool(name="replace_text_in_file")
|
3
|
+
from janito.agent.tools.tools_utils import pluralize
|
5
4
|
|
6
5
|
|
6
|
+
@register_tool(name="replace_text_in_file")
|
7
7
|
class ReplaceTextInFileTool(ToolBase):
|
8
|
-
"""
|
8
|
+
"""
|
9
|
+
Replace exact occurrences of a given text in a file.
|
9
10
|
|
10
|
-
|
11
|
+
Args:
|
12
|
+
file_path (str): Path to the file to modify.
|
13
|
+
search_text (str): The exact text to search for (including indentation).
|
14
|
+
replacement_text (str): The text to replace with (including indentation).
|
15
|
+
replace_all (bool): If True, replace all occurrences; otherwise, only the first occurrence.
|
16
|
+
backup (bool, optional): If True, create a backup (.bak) before replacing. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
|
17
|
+
Returns:
|
18
|
+
str: Status message. Example:
|
19
|
+
- "Text replaced in /path/to/file (backup at /path/to/file.bak)"
|
20
|
+
- "No changes made. [Warning: Search text not found in file] Please review the original file."
|
21
|
+
- "Error replacing text: <error message>"
|
22
|
+
"""
|
11
23
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
search_text (str): Text to search for. Must include indentation (leading whitespace) if present in the file.
|
21
|
-
replacement_text (str): Replacement text. Must include desired indentation (leading whitespace).
|
22
|
-
replace_all (bool): If True, replace all occurrences; otherwise, only the first occurrence.
|
23
|
-
Returns:
|
24
|
-
str: Status message. Example:
|
25
|
-
- "Text replaced in /path/to/file"
|
26
|
-
- "No changes made. [Warning: Search text not found in file] Please review the original file."
|
27
|
-
- "Error replacing text: <error message>"
|
28
|
-
"""
|
24
|
+
def call(
|
25
|
+
self,
|
26
|
+
file_path: str,
|
27
|
+
search_text: str,
|
28
|
+
replacement_text: str,
|
29
|
+
replace_all: bool = False,
|
30
|
+
backup: bool = False,
|
31
|
+
) -> str:
|
29
32
|
from janito.agent.tools.tools_utils import display_path
|
33
|
+
|
30
34
|
disp_path = display_path(file_path)
|
31
35
|
action = "all occurrences" if replace_all else None
|
32
36
|
# Show only concise info (lengths, not full content)
|
33
|
-
search_preview = (
|
34
|
-
|
37
|
+
search_preview = (
|
38
|
+
(search_text[:20] + "...") if len(search_text) > 20 else search_text
|
39
|
+
)
|
40
|
+
replace_preview = (
|
41
|
+
(replacement_text[:20] + "...")
|
42
|
+
if len(replacement_text) > 20
|
43
|
+
else replacement_text
|
44
|
+
)
|
35
45
|
search_lines = len(search_text.splitlines())
|
36
46
|
replace_lines = len(replacement_text.splitlines())
|
37
|
-
info_msg = f"
|
47
|
+
info_msg = f"\U0001f4dd Replacing in {disp_path}: {search_lines}\u2192{replace_lines} lines"
|
38
48
|
if action:
|
39
49
|
info_msg += f" ({action})"
|
40
50
|
self.report_info(info_msg)
|
41
51
|
|
42
52
|
try:
|
43
|
-
with open(file_path,
|
53
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
44
54
|
content = f.read()
|
45
55
|
|
46
56
|
if replace_all:
|
@@ -49,42 +59,59 @@ NOTE: Indentation (leading whitespace) must be included in both search_text and
|
|
49
59
|
else:
|
50
60
|
occurrences = content.count(search_text)
|
51
61
|
if occurrences > 1:
|
52
|
-
self.report_warning("
|
62
|
+
self.report_warning("\u26a0\ufe0f Search text is not unique.")
|
53
63
|
warning_detail = "The search text is not unique. Expand your search context with surrounding lines to ensure uniqueness."
|
54
64
|
return f"No changes made. {warning_detail}"
|
55
65
|
replaced_count = 1 if occurrences == 1 else 0
|
56
66
|
new_content = content.replace(search_text, replacement_text, 1)
|
67
|
+
import shutil
|
68
|
+
|
69
|
+
backup_path = file_path + ".bak"
|
70
|
+
if backup and new_content != content:
|
71
|
+
# Create a .bak backup before writing changes
|
72
|
+
shutil.copy2(file_path, backup_path)
|
57
73
|
if new_content != content:
|
58
|
-
with open(file_path,
|
74
|
+
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
59
75
|
f.write(new_content)
|
60
76
|
file_changed = True
|
61
77
|
else:
|
62
78
|
file_changed = False
|
63
|
-
warning =
|
79
|
+
warning = ""
|
64
80
|
if replaced_count == 0:
|
65
81
|
warning = " [Warning: Search text not found in file]"
|
66
82
|
if not file_changed:
|
67
|
-
self.report_warning("
|
83
|
+
self.report_warning(" \u2139\ufe0f No changes made.")
|
68
84
|
concise_warning = "The search text was not found. Expand your search context with surrounding lines if needed."
|
69
85
|
return f"No changes made. {concise_warning}"
|
70
|
-
|
71
|
-
self.report_success(
|
86
|
+
|
87
|
+
self.report_success(
|
88
|
+
f" \u2705 {replaced_count} {pluralize('block', replaced_count)} replaced"
|
89
|
+
)
|
90
|
+
|
72
91
|
# Indentation check for agent warning
|
73
92
|
def leading_ws(line):
|
74
93
|
import re
|
94
|
+
|
75
95
|
m = re.match(r"^\s*", line)
|
76
|
-
return m.group(0) if m else
|
77
|
-
|
78
|
-
|
79
|
-
|
96
|
+
return m.group(0) if m else ""
|
97
|
+
|
98
|
+
search_indent = (
|
99
|
+
leading_ws(search_text.splitlines()[0])
|
100
|
+
if search_text.splitlines()
|
101
|
+
else ""
|
102
|
+
)
|
103
|
+
replace_indent = (
|
104
|
+
leading_ws(replacement_text.splitlines()[0])
|
105
|
+
if replacement_text.splitlines()
|
106
|
+
else ""
|
107
|
+
)
|
108
|
+
indent_warning = ""
|
80
109
|
if search_indent != replace_indent:
|
81
110
|
indent_warning = f" [Warning: Indentation mismatch between search and replacement text: '{search_indent}' vs '{replace_indent}']"
|
82
|
-
if
|
83
|
-
return f"Text replaced in {file_path}{warning}{indent_warning}\n{warning_detail}"
|
84
|
-
return f"Text replaced in {file_path}{warning}{indent_warning}"
|
111
|
+
if "warning_detail" in locals():
|
112
|
+
return f"Text replaced in {file_path}{warning}{indent_warning} (backup at {backup_path})\n{warning_detail}"
|
113
|
+
return f"Text replaced in {file_path}{warning}{indent_warning} (backup at {backup_path})"
|
85
114
|
|
86
115
|
except Exception as e:
|
87
|
-
self.report_error("
|
116
|
+
self.report_error(" \u274c Error")
|
88
117
|
return f"Error replacing text: {e}"
|
89
|
-
|
90
|
-
from janito.agent.tools.tools_utils import pluralize
|
janito/agent/tools/rich_live.py
CHANGED
@@ -7,23 +7,28 @@ console = Console()
|
|
7
7
|
|
8
8
|
_global_live = None
|
9
9
|
|
10
|
+
|
10
11
|
@contextmanager
|
11
12
|
def global_live_panel(title="Working..."):
|
12
13
|
global _global_live
|
13
14
|
if _global_live is None:
|
14
|
-
_global_live = Live(
|
15
|
+
_global_live = Live(
|
16
|
+
Panel("", title=title), console=console, refresh_per_second=4
|
17
|
+
)
|
15
18
|
_global_live.start()
|
16
19
|
try:
|
17
20
|
yield _global_live
|
18
21
|
finally:
|
19
22
|
pass # Do not stop here; stopping is handled explicitly
|
20
23
|
|
24
|
+
|
21
25
|
def stop_global_live_panel():
|
22
26
|
global _global_live
|
23
27
|
if _global_live is not None:
|
24
28
|
_global_live.stop()
|
25
29
|
_global_live = None
|
26
30
|
|
31
|
+
|
27
32
|
@contextmanager
|
28
33
|
def live_panel(title="Working..."):
|
29
34
|
global _global_live
|
@@ -33,5 +38,7 @@ def live_panel(title="Working..."):
|
|
33
38
|
yield _global_live
|
34
39
|
else:
|
35
40
|
# Fallback: create a temporary panel if no global panel is running
|
36
|
-
with Live(
|
41
|
+
with Live(
|
42
|
+
Panel("", title=title), console=console, refresh_per_second=4
|
43
|
+
) as live:
|
37
44
|
yield live
|
@@ -1,107 +1,155 @@
|
|
1
|
-
from janito.agent.
|
2
|
-
from janito.agent.tool_registry import register_tool
|
3
|
-
|
4
|
-
import subprocess
|
5
|
-
import tempfile
|
6
|
-
import sys
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
1
|
+
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_registry import register_tool
|
3
|
+
|
4
|
+
import subprocess
|
5
|
+
import tempfile
|
6
|
+
import sys
|
7
|
+
|
8
|
+
|
9
|
+
@register_tool(name="run_bash_command")
|
10
|
+
class RunBashCommandTool(ToolBase):
|
11
|
+
"""
|
12
|
+
Execute a non-interactive command using the bash shell and capture live output.
|
13
|
+
|
14
|
+
This tool explicitly invokes the 'bash' shell (not just the system default shell), so it requires bash to be installed and available in the system PATH. On Windows, this will only work if bash is available (e.g., via WSL, Git Bash, or similar).
|
15
|
+
|
16
|
+
Args:
|
17
|
+
command (str): The bash command to execute.
|
18
|
+
timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
|
19
|
+
require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
|
20
|
+
interactive (bool, optional): If True, warns that the command may require user interaction. Defaults to False. Non-interactive commands are preferred for automation and reliability.
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
str: File paths and line counts for stdout and stderr.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def call(
|
27
|
+
self,
|
28
|
+
command: str,
|
29
|
+
timeout: int = 60,
|
30
|
+
require_confirmation: bool = False,
|
31
|
+
interactive: bool = False,
|
32
|
+
) -> str:
|
33
|
+
"""
|
34
|
+
Execute a bash command and capture live output.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
command (str): The bash command to execute.
|
38
|
+
timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
|
39
|
+
require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
|
40
|
+
interactive (bool, optional): If True, warns that the command may require user interaction. Defaults to False.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
str: Output and status message.
|
44
|
+
"""
|
45
|
+
"""
|
46
|
+
Execute a bash command and capture live output.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
command (str): The bash command to execute.
|
50
|
+
timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
|
51
|
+
require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
|
52
|
+
interactive (bool, optional): If True, warns that the command may require user interaction. Defaults to False.
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
str: Output and status message.
|
56
|
+
"""
|
57
|
+
if not command.strip():
|
58
|
+
self.report_warning("⚠️ Warning: Empty command provided. Operation skipped.")
|
59
|
+
return "Warning: Empty command provided. Operation skipped."
|
60
|
+
self.report_info(f"🖥️ Running bash command: {command}\n")
|
61
|
+
if interactive:
|
62
|
+
self.report_info(
|
63
|
+
"⚠️ Warning: This command might be interactive, require user input, and might hang."
|
64
|
+
)
|
65
|
+
|
66
|
+
sys.stdout.flush()
|
67
|
+
|
68
|
+
try:
|
69
|
+
with (
|
70
|
+
tempfile.NamedTemporaryFile(
|
71
|
+
mode="w+", prefix="run_bash_stdout_", delete=False, encoding="utf-8"
|
72
|
+
) as stdout_file,
|
73
|
+
tempfile.NamedTemporaryFile(
|
74
|
+
mode="w+", prefix="run_bash_stderr_", delete=False, encoding="utf-8"
|
75
|
+
) as stderr_file,
|
76
|
+
):
|
77
|
+
# Use bash explicitly for command execution
|
78
|
+
process = subprocess.Popen(
|
79
|
+
["bash", "-c", command],
|
80
|
+
stdout=stdout_file,
|
81
|
+
stderr=stderr_file,
|
82
|
+
text=True,
|
83
|
+
)
|
84
|
+
try:
|
85
|
+
return_code = process.wait(timeout=timeout)
|
86
|
+
except subprocess.TimeoutExpired:
|
87
|
+
process.kill()
|
88
|
+
self.report_error(f" ❌ Timed out after {timeout} seconds.")
|
89
|
+
return f"Command timed out after {timeout} seconds."
|
90
|
+
|
91
|
+
# Print live output to user
|
92
|
+
stdout_file.flush()
|
93
|
+
stderr_file.flush()
|
94
|
+
with open(
|
95
|
+
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
96
|
+
) as out_f:
|
97
|
+
out_f.seek(0)
|
98
|
+
for line in out_f:
|
99
|
+
self.report_stdout(line)
|
100
|
+
with open(
|
101
|
+
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
102
|
+
) as err_f:
|
103
|
+
err_f.seek(0)
|
104
|
+
for line in err_f:
|
105
|
+
self.report_stderr(line)
|
106
|
+
|
107
|
+
# Count lines
|
108
|
+
with open(
|
109
|
+
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
110
|
+
) as out_f:
|
111
|
+
stdout_lines = sum(1 for _ in out_f)
|
112
|
+
with open(
|
113
|
+
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
114
|
+
) as err_f:
|
115
|
+
stderr_lines = sum(1 for _ in err_f)
|
116
|
+
|
117
|
+
self.report_success(f" ✅ return code {return_code}")
|
118
|
+
warning_msg = ""
|
119
|
+
if interactive:
|
120
|
+
warning_msg = "⚠️ Warning: This command might be interactive, require user input, and might hang.\n"
|
121
|
+
|
122
|
+
# Read output contents
|
123
|
+
with open(
|
124
|
+
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
125
|
+
) as out_f:
|
126
|
+
stdout_content = out_f.read()
|
127
|
+
with open(
|
128
|
+
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
129
|
+
) as err_f:
|
130
|
+
stderr_content = err_f.read()
|
131
|
+
|
132
|
+
# Thresholds
|
133
|
+
max_lines = 100
|
134
|
+
if stdout_lines <= max_lines and stderr_lines <= max_lines:
|
135
|
+
result = (
|
136
|
+
warning_msg
|
137
|
+
+ f"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}"
|
138
|
+
)
|
139
|
+
if stderr_content.strip():
|
140
|
+
result += f"\n--- STDERR ---\n{stderr_content}"
|
141
|
+
return result
|
142
|
+
else:
|
143
|
+
result = (
|
144
|
+
warning_msg
|
145
|
+
+ f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
|
146
|
+
)
|
147
|
+
if stderr_lines > 0 and stderr_content.strip():
|
148
|
+
result += (
|
149
|
+
f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
|
150
|
+
)
|
151
|
+
result += f"returncode: {return_code}\nUse the get_lines tool to inspect the contents of these files when needed."
|
152
|
+
return result
|
153
|
+
except Exception as e:
|
154
|
+
self.report_error(f" ❌ Error: {e}")
|
155
|
+
return f"Error running command: {e}"
|
@@ -0,0 +1,139 @@
|
|
1
|
+
import subprocess
|
2
|
+
import tempfile
|
3
|
+
import sys
|
4
|
+
from janito.agent.tool_base import ToolBase
|
5
|
+
from janito.agent.tool_registry import register_tool
|
6
|
+
|
7
|
+
|
8
|
+
@register_tool(name="run_python_command")
|
9
|
+
class RunPythonCommandTool(ToolBase):
|
10
|
+
"""
|
11
|
+
Tool to execute Python code in a subprocess and capture output.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
code (str): The Python code to execute.
|
15
|
+
timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
|
16
|
+
require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
|
17
|
+
interactive (bool, optional): If True, warns that the command may require user interaction. Defaults to False.
|
18
|
+
Returns:
|
19
|
+
str: File paths and line counts for stdout and stderr, or direct output if small enough.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def call(
|
23
|
+
self,
|
24
|
+
code: str,
|
25
|
+
timeout: int = 60,
|
26
|
+
require_confirmation: bool = False,
|
27
|
+
interactive: bool = False,
|
28
|
+
) -> str:
|
29
|
+
if not code.strip():
|
30
|
+
self.report_warning("⚠️ Warning: Empty code provided. Operation skipped.")
|
31
|
+
return "Warning: Empty code provided. Operation skipped."
|
32
|
+
self.report_info(f"🐍 Running Python code:\n{code}\n")
|
33
|
+
if interactive:
|
34
|
+
self.report_info(
|
35
|
+
"⚠️ Warning: This code might be interactive, require user input, and might hang."
|
36
|
+
)
|
37
|
+
sys.stdout.flush()
|
38
|
+
if require_confirmation:
|
39
|
+
confirmed = self.confirm_action("Do you want to execute this Python code?")
|
40
|
+
if not confirmed:
|
41
|
+
self.report_warning("Execution cancelled by user.")
|
42
|
+
return "Execution cancelled by user."
|
43
|
+
try:
|
44
|
+
with (
|
45
|
+
tempfile.NamedTemporaryFile(
|
46
|
+
mode="w+",
|
47
|
+
suffix=".py",
|
48
|
+
prefix="run_python_",
|
49
|
+
delete=False,
|
50
|
+
encoding="utf-8",
|
51
|
+
) as code_file,
|
52
|
+
tempfile.NamedTemporaryFile(
|
53
|
+
mode="w+",
|
54
|
+
prefix="run_python_stdout_",
|
55
|
+
delete=False,
|
56
|
+
encoding="utf-8",
|
57
|
+
) as stdout_file,
|
58
|
+
tempfile.NamedTemporaryFile(
|
59
|
+
mode="w+",
|
60
|
+
prefix="run_python_stderr_",
|
61
|
+
delete=False,
|
62
|
+
encoding="utf-8",
|
63
|
+
) as stderr_file,
|
64
|
+
):
|
65
|
+
code_file.write(code)
|
66
|
+
code_file.flush()
|
67
|
+
process = subprocess.Popen(
|
68
|
+
[sys.executable, code_file.name],
|
69
|
+
stdout=stdout_file,
|
70
|
+
stderr=stderr_file,
|
71
|
+
text=True,
|
72
|
+
)
|
73
|
+
try:
|
74
|
+
return_code = process.wait(timeout=timeout)
|
75
|
+
except subprocess.TimeoutExpired:
|
76
|
+
process.kill()
|
77
|
+
self.report_error(f" ❌ Timed out after {timeout} seconds.")
|
78
|
+
return f"Code timed out after {timeout} seconds."
|
79
|
+
# Print live output to user
|
80
|
+
stdout_file.flush()
|
81
|
+
stderr_file.flush()
|
82
|
+
with open(
|
83
|
+
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
84
|
+
) as out_f:
|
85
|
+
out_f.seek(0)
|
86
|
+
for line in out_f:
|
87
|
+
self.report_stdout(line)
|
88
|
+
with open(
|
89
|
+
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
90
|
+
) as err_f:
|
91
|
+
err_f.seek(0)
|
92
|
+
for line in err_f:
|
93
|
+
self.report_stderr(line)
|
94
|
+
# Count lines
|
95
|
+
with open(
|
96
|
+
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
97
|
+
) as out_f:
|
98
|
+
stdout_lines = sum(1 for _ in out_f)
|
99
|
+
with open(
|
100
|
+
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
101
|
+
) as err_f:
|
102
|
+
stderr_lines = sum(1 for _ in err_f)
|
103
|
+
self.report_success(f" ✅ return code {return_code}")
|
104
|
+
warning_msg = ""
|
105
|
+
if interactive:
|
106
|
+
warning_msg = "⚠️ Warning: This code might be interactive, require user input, and might hang.\n"
|
107
|
+
# Read output contents
|
108
|
+
with open(
|
109
|
+
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
110
|
+
) as out_f:
|
111
|
+
stdout_content = out_f.read()
|
112
|
+
with open(
|
113
|
+
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
114
|
+
) as err_f:
|
115
|
+
stderr_content = err_f.read()
|
116
|
+
# Thresholds
|
117
|
+
max_lines = 100
|
118
|
+
if stdout_lines <= max_lines and stderr_lines <= max_lines:
|
119
|
+
result = (
|
120
|
+
warning_msg
|
121
|
+
+ f"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}"
|
122
|
+
)
|
123
|
+
if stderr_content.strip():
|
124
|
+
result += f"\n--- STDERR ---\n{stderr_content}"
|
125
|
+
return result
|
126
|
+
else:
|
127
|
+
result = (
|
128
|
+
warning_msg
|
129
|
+
+ f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
|
130
|
+
)
|
131
|
+
if stderr_lines > 0 and stderr_content.strip():
|
132
|
+
result += (
|
133
|
+
f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
|
134
|
+
)
|
135
|
+
result += f"returncode: {return_code}\nUse the get_lines tool to inspect the contents of these files when needed."
|
136
|
+
return result
|
137
|
+
except Exception as e:
|
138
|
+
self.report_error(f" ❌ Error: {e}")
|
139
|
+
return f"Error running code: {e}"
|