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.
Files changed (85) hide show
  1. janito/__init__.py +1 -1
  2. janito/__main__.py +0 -1
  3. janito/agent/config.py +11 -10
  4. janito/agent/config_defaults.py +3 -2
  5. janito/agent/conversation.py +93 -119
  6. janito/agent/conversation_api.py +98 -0
  7. janito/agent/conversation_exceptions.py +12 -0
  8. janito/agent/conversation_tool_calls.py +22 -0
  9. janito/agent/conversation_ui.py +17 -0
  10. janito/agent/message_handler.py +8 -9
  11. janito/agent/{agent.py → openai_client.py} +48 -16
  12. janito/agent/openai_schema_generator.py +53 -37
  13. janito/agent/profile_manager.py +172 -0
  14. janito/agent/queued_message_handler.py +13 -14
  15. janito/agent/rich_live.py +32 -0
  16. janito/agent/rich_message_handler.py +64 -0
  17. janito/agent/runtime_config.py +6 -1
  18. janito/agent/{tools/tool_base.py → tool_base.py} +15 -8
  19. janito/agent/tool_registry.py +118 -132
  20. janito/agent/tools/__init__.py +41 -2
  21. janito/agent/tools/ask_user.py +43 -33
  22. janito/agent/tools/create_directory.py +18 -16
  23. janito/agent/tools/create_file.py +31 -36
  24. janito/agent/tools/fetch_url.py +23 -19
  25. janito/agent/tools/find_files.py +40 -36
  26. janito/agent/tools/get_file_outline.py +100 -22
  27. janito/agent/tools/get_lines.py +40 -32
  28. janito/agent/tools/gitignore_utils.py +9 -6
  29. janito/agent/tools/move_file.py +22 -13
  30. janito/agent/tools/py_compile_file.py +40 -0
  31. janito/agent/tools/remove_directory.py +34 -24
  32. janito/agent/tools/remove_file.py +22 -20
  33. janito/agent/tools/replace_file.py +51 -0
  34. janito/agent/tools/replace_text_in_file.py +69 -42
  35. janito/agent/tools/rich_live.py +9 -2
  36. janito/agent/tools/run_bash_command.py +155 -107
  37. janito/agent/tools/run_python_command.py +139 -0
  38. janito/agent/tools/search_files.py +51 -34
  39. janito/agent/tools/tools_utils.py +4 -2
  40. janito/agent/tools/utils.py +6 -2
  41. janito/cli/_print_config.py +42 -16
  42. janito/cli/_utils.py +1 -0
  43. janito/cli/arg_parser.py +182 -29
  44. janito/cli/config_commands.py +54 -22
  45. janito/cli/logging_setup.py +9 -3
  46. janito/cli/main.py +11 -10
  47. janito/cli/runner/__init__.py +2 -0
  48. janito/cli/runner/cli_main.py +148 -0
  49. janito/cli/runner/config.py +33 -0
  50. janito/cli/runner/formatting.py +12 -0
  51. janito/cli/runner/scan.py +44 -0
  52. janito/cli_chat_shell/__init__.py +0 -1
  53. janito/cli_chat_shell/chat_loop.py +71 -92
  54. janito/cli_chat_shell/chat_state.py +38 -0
  55. janito/cli_chat_shell/chat_ui.py +43 -0
  56. janito/cli_chat_shell/commands/__init__.py +45 -0
  57. janito/cli_chat_shell/commands/config.py +22 -0
  58. janito/cli_chat_shell/commands/history_reset.py +29 -0
  59. janito/cli_chat_shell/commands/session.py +48 -0
  60. janito/cli_chat_shell/commands/session_control.py +12 -0
  61. janito/cli_chat_shell/commands/system.py +73 -0
  62. janito/cli_chat_shell/commands/utility.py +29 -0
  63. janito/cli_chat_shell/config_shell.py +39 -10
  64. janito/cli_chat_shell/load_prompt.py +5 -2
  65. janito/cli_chat_shell/session_manager.py +24 -27
  66. janito/cli_chat_shell/ui.py +75 -40
  67. janito/rich_utils.py +15 -2
  68. janito/web/__main__.py +10 -2
  69. janito/web/app.py +88 -52
  70. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/METADATA +76 -11
  71. janito-1.6.0.dist-info/RECORD +81 -0
  72. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/WHEEL +1 -1
  73. janito/agent/rich_tool_handler.py +0 -43
  74. janito/agent/templates/system_instructions.j2 +0 -38
  75. janito/agent/tool_auto_imports.py +0 -5
  76. janito/agent/tools/append_text_to_file.py +0 -41
  77. janito/agent/tools/py_compile.py +0 -39
  78. janito/agent/tools/python_exec.py +0 -83
  79. janito/cli/runner.py +0 -137
  80. janito/cli_chat_shell/commands.py +0 -204
  81. janito/render_prompt.py +0 -13
  82. janito-1.5.2.dist-info/RECORD +0 -66
  83. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/entry_points.txt +0 -0
  84. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/licenses/LICENSE +0 -0
  85. {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.tools.tool_base import ToolBase
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
- """Replace exact occurrences of a given text in a file.
8
+ """
9
+ Replace exact occurrences of a given text in a file.
9
10
 
10
- This tool is designed to make minimal, targeted changes—preferably a small region modifications—rather than rewriting large sections or the entire file. Use it for precise, context-aware edits.
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
- NOTE: Indentation (leading whitespace) must be included in both search_text and replacement_text. This tool does not automatically adjust or infer indentation; matches are exact, including whitespace.
13
- """
14
- def call(self, file_path: str, search_text: str, replacement_text: str, replace_all: bool = False) -> str:
15
- """
16
- Replace exact occurrences of a given text in a file.
17
-
18
- Args:
19
- file_path (str): Path to the file.
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 = (search_text[:20] + '...') if len(search_text) > 20 else search_text
34
- replace_preview = (replacement_text[:20] + '...') if len(replacement_text) > 20 else replacement_text
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"📝 Replacing in {disp_path}: {search_lines}{replace_lines} lines"
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, 'r', encoding='utf-8') as f:
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("⚠️ Search text is not unique.")
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, 'w', encoding='utf-8') as f:
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(" No changes made.")
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(f" ✅ {replaced_count} {pluralize('block', replaced_count)} replaced")
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
- search_indent = leading_ws(search_text.splitlines()[0]) if search_text.splitlines() else ''
78
- replace_indent = leading_ws(replacement_text.splitlines()[0]) if replacement_text.splitlines() else ''
79
- indent_warning = ''
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 'warning_detail' in locals():
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(" 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
@@ -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(Panel("", title=title), console=console, refresh_per_second=4)
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(Panel("", title=title), console=console, refresh_per_second=4) as 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.tools.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
- @register_tool(name="run_bash_command")
9
- class RunBashCommandTool(ToolBase):
10
- """
11
- Execute a non-interactive command using the bash shell and capture live output.
12
-
13
- 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).
14
-
15
- Args:
16
- command (str): The bash command to execute.
17
- timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
18
- require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
19
- 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.
20
-
21
- Returns:
22
- str: File paths and line counts for stdout and stderr.
23
- """
24
- def call(self, command: str, timeout: int = 60, require_confirmation: bool = False, interactive: bool = False) -> str:
25
- """
26
- Execute a bash command and capture live output.
27
-
28
- Args:
29
- command (str): The bash command to execute.
30
- timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
31
- require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
32
- interactive (bool, optional): If True, warns that the command may require user interaction. Defaults to False.
33
-
34
- Returns:
35
- str: Output and status message.
36
- """
37
- """
38
- Execute a bash command and capture live output.
39
-
40
- Args:
41
- command (str): The bash command to execute.
42
- timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
43
- require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
44
- interactive (bool, optional): If True, warns that the command may require user interaction. Defaults to False.
45
-
46
- Returns:
47
- str: Output and status message.
48
- """
49
- if not command.strip():
50
- self.report_warning("⚠️ Warning: Empty command provided. Operation skipped.")
51
- return "Warning: Empty command provided. Operation skipped."
52
- self.report_info(f"🖥️ Running bash command: {command}\n")
53
- if interactive:
54
- self.report_info("⚠️ Warning: This command might be interactive, require user input, and might hang.")
55
-
56
- sys.stdout.flush()
57
-
58
- try:
59
- with tempfile.NamedTemporaryFile(mode='w+', prefix='run_bash_stdout_', delete=False, encoding='utf-8') as stdout_file, \
60
- tempfile.NamedTemporaryFile(mode='w+', prefix='run_bash_stderr_', delete=False, encoding='utf-8') as stderr_file:
61
- # Use bash explicitly for command execution
62
- process = subprocess.Popen(
63
- ["bash", "-c", command],
64
- stdout=stdout_file,
65
- stderr=stderr_file,
66
- text=True
67
- )
68
- try:
69
- return_code = process.wait(timeout=timeout)
70
- except subprocess.TimeoutExpired:
71
- process.kill()
72
- self.report_error(f" Timed out after {timeout} seconds.")
73
- return f"Command timed out after {timeout} seconds."
74
-
75
- # Print live output to user
76
- stdout_file.flush()
77
- stderr_file.flush()
78
- with open(stdout_file.name, 'r', encoding='utf-8') as out_f:
79
- out_f.seek(0)
80
- for line in out_f:
81
- self.report_stdout(line)
82
- with open(stderr_file.name, 'r', encoding='utf-8') as err_f:
83
- err_f.seek(0)
84
- for line in err_f:
85
- self.report_stderr(line)
86
-
87
- # Count lines
88
- with open(stdout_file.name, 'r', encoding='utf-8') as out_f:
89
- stdout_lines = sum(1 for _ in out_f)
90
- with open(stderr_file.name, 'r', encoding='utf-8') as err_f:
91
- stderr_lines = sum(1 for _ in err_f)
92
-
93
- self.report_success(f" ✅ return code {return_code}")
94
- warning_msg = ""
95
- if interactive:
96
- warning_msg = "⚠️ Warning: This command might be interactive, require user input, and might hang.\n"
97
- return (
98
- warning_msg +
99
- f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
100
- f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
101
- f"returncode: {return_code}\n"
102
- f"Use the get_lines tool to inspect the contents of these files when needed."
103
- )
104
- except Exception as e:
105
- self.report_error(f" ❌ Error: {e}")
106
- return f"Error running command: {e}"
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}"