janito 1.8.1__py3-none-any.whl → 1.10.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 (142) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/api_exceptions.py +4 -0
  3. janito/agent/config.py +1 -1
  4. janito/agent/config_defaults.py +2 -3
  5. janito/agent/config_utils.py +0 -9
  6. janito/agent/conversation.py +177 -114
  7. janito/agent/conversation_api.py +179 -159
  8. janito/agent/conversation_tool_calls.py +11 -8
  9. janito/agent/llm_conversation_history.py +70 -0
  10. janito/agent/openai_client.py +44 -21
  11. janito/agent/openai_schema_generator.py +164 -128
  12. janito/agent/platform_discovery.py +134 -77
  13. janito/agent/profile_manager.py +5 -5
  14. janito/agent/rich_message_handler.py +80 -31
  15. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +9 -8
  16. janito/agent/test_openai_schema_generator.py +93 -0
  17. janito/agent/tool_base.py +7 -2
  18. janito/agent/tool_executor.py +63 -50
  19. janito/agent/tool_registry.py +5 -2
  20. janito/agent/tool_use_tracker.py +42 -5
  21. janito/agent/tools/__init__.py +13 -12
  22. janito/agent/tools/create_directory.py +9 -6
  23. janito/agent/tools/create_file.py +35 -54
  24. janito/agent/tools/delete_text_in_file.py +97 -0
  25. janito/agent/tools/fetch_url.py +50 -5
  26. janito/agent/tools/find_files.py +40 -26
  27. janito/agent/tools/get_file_outline/__init__.py +1 -0
  28. janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +14 -18
  29. janito/agent/tools/get_file_outline/python_outline.py +134 -0
  30. janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +11 -0
  31. janito/agent/tools/get_lines.py +21 -12
  32. janito/agent/tools/move_file.py +13 -12
  33. janito/agent/tools/present_choices.py +3 -1
  34. janito/agent/tools/python_command_runner.py +150 -0
  35. janito/agent/tools/python_file_runner.py +148 -0
  36. janito/agent/tools/python_stdin_runner.py +154 -0
  37. janito/agent/tools/remove_directory.py +4 -2
  38. janito/agent/tools/remove_file.py +15 -13
  39. janito/agent/tools/replace_file.py +72 -0
  40. janito/agent/tools/replace_text_in_file.py +7 -5
  41. janito/agent/tools/run_bash_command.py +29 -72
  42. janito/agent/tools/run_powershell_command.py +142 -102
  43. janito/agent/tools/search_text.py +177 -131
  44. janito/agent/tools/validate_file_syntax/__init__.py +1 -0
  45. janito/agent/tools/validate_file_syntax/core.py +94 -0
  46. janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
  47. janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
  48. janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
  49. janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
  50. janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
  51. janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
  52. janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
  53. janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
  54. janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
  55. janito/agent/tools_utils/__init__.py +1 -0
  56. janito/agent/tools_utils/action_type.py +7 -0
  57. janito/agent/tools_utils/dir_walk_utils.py +24 -0
  58. janito/agent/tools_utils/formatting.py +49 -0
  59. janito/agent/tools_utils/gitignore_utils.py +69 -0
  60. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  61. janito/agent/tools_utils/utils.py +30 -0
  62. janito/cli/_livereload_log_utils.py +13 -0
  63. janito/cli/_print_config.py +63 -61
  64. janito/cli/arg_parser.py +57 -14
  65. janito/cli/cli_main.py +270 -0
  66. janito/cli/livereload_starter.py +60 -0
  67. janito/cli/main.py +166 -99
  68. janito/cli/one_shot.py +80 -0
  69. janito/cli/termweb_starter.py +2 -2
  70. janito/i18n/__init__.py +1 -1
  71. janito/livereload/app.py +25 -0
  72. janito/rich_utils.py +41 -25
  73. janito/{cli_chat_shell → shell}/commands/__init__.py +19 -14
  74. janito/{cli_chat_shell → shell}/commands/config.py +4 -4
  75. janito/shell/commands/conversation_restart.py +74 -0
  76. janito/shell/commands/edit.py +24 -0
  77. janito/shell/commands/history_view.py +18 -0
  78. janito/{cli_chat_shell → shell}/commands/lang.py +3 -0
  79. janito/shell/commands/livelogs.py +42 -0
  80. janito/{cli_chat_shell → shell}/commands/prompt.py +16 -6
  81. janito/shell/commands/session.py +35 -0
  82. janito/{cli_chat_shell → shell}/commands/session_control.py +3 -5
  83. janito/{cli_chat_shell → shell}/commands/termweb_log.py +18 -10
  84. janito/shell/commands/tools.py +26 -0
  85. janito/shell/commands/track.py +36 -0
  86. janito/shell/commands/utility.py +28 -0
  87. janito/{cli_chat_shell → shell}/commands/verbose.py +4 -5
  88. janito/shell/commands.py +40 -0
  89. janito/shell/input_history.py +62 -0
  90. janito/shell/main.py +257 -0
  91. janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
  92. janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
  93. janito/shell/session/manager.py +101 -0
  94. janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -17
  95. janito/termweb/app.py +3 -3
  96. janito/termweb/static/editor.css +142 -0
  97. janito/termweb/static/editor.css.bak +27 -0
  98. janito/termweb/static/editor.html +15 -213
  99. janito/termweb/static/editor.html.bak +16 -215
  100. janito/termweb/static/editor.js +209 -0
  101. janito/termweb/static/editor.js.bak +227 -0
  102. janito/termweb/static/index.html +2 -3
  103. janito/termweb/static/index.html.bak +2 -3
  104. janito/termweb/static/termweb.css.bak +33 -84
  105. janito/termweb/static/termweb.js +15 -34
  106. janito/termweb/static/termweb.js.bak +18 -36
  107. janito/tests/test_rich_utils.py +44 -0
  108. janito/web/app.py +0 -75
  109. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/METADATA +62 -42
  110. janito-1.10.0.dist-info/RECORD +158 -0
  111. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
  112. janito/agent/tools/dir_walk_utils.py +0 -16
  113. janito/agent/tools/gitignore_utils.py +0 -46
  114. janito/agent/tools/memory.py +0 -48
  115. janito/agent/tools/outline_file/formatting.py +0 -20
  116. janito/agent/tools/outline_file/python_outline.py +0 -71
  117. janito/agent/tools/present_choices_test.py +0 -18
  118. janito/agent/tools/rich_live.py +0 -44
  119. janito/agent/tools/run_python_command.py +0 -163
  120. janito/agent/tools/tools_utils.py +0 -56
  121. janito/agent/tools/utils.py +0 -33
  122. janito/agent/tools/validate_file_syntax.py +0 -163
  123. janito/cli/runner/cli_main.py +0 -180
  124. janito/cli_chat_shell/chat_loop.py +0 -163
  125. janito/cli_chat_shell/chat_state.py +0 -38
  126. janito/cli_chat_shell/commands/history_start.py +0 -37
  127. janito/cli_chat_shell/commands/session.py +0 -48
  128. janito/cli_chat_shell/commands/sum.py +0 -49
  129. janito/cli_chat_shell/commands/utility.py +0 -32
  130. janito/cli_chat_shell/session_manager.py +0 -72
  131. janito-1.8.1.dist-info/RECORD +0 -127
  132. /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
  133. /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
  134. /janito/cli/{runner/config.py → config_runner.py} +0 -0
  135. /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
  136. /janito/{cli/runner → shell}/__init__.py +0 -0
  137. /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
  138. /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
  139. /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
  140. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
  141. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
  142. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
@@ -1,40 +1,49 @@
1
1
  from janito.agent.tool_base import ToolBase
2
+ from janito.agent.tools_utils.action_type import ActionType
2
3
  from janito.agent.tool_registry import register_tool
3
- from janito.agent.tools.tools_utils import pluralize
4
+ from janito.agent.tools_utils.utils import pluralize
4
5
  from janito.i18n import tr
5
6
 
6
7
 
7
8
  @register_tool(name="get_lines")
8
9
  class GetLinesTool(ToolBase):
9
10
  """
10
- Read lines from a file. Returns specific lines if a range is provided, or the entire file if no range is given. If both from_line and to_line are None, the entire file is returned in one call—no need to chunk or split requests when reading the full file.
11
+ Read lines from a file. You can specify a line range, or read the entire file by simply omitting the from_line and to_line parameters.
12
+
11
13
  Args:
12
14
  file_path (str): Path to the file to read lines from.
13
- from_line (int, optional): Starting line number (1-based). If None, starts from the first line.
14
- to_line (int, optional): Ending line number (1-based). If None, reads to the end of the file. If both are None, the entire file is returned.
15
+ from_line (int, optional): Starting line number (1-based). Omit to start from the first line.
16
+ to_line (int, optional): Ending line number (1-based). Omit to read to the end of the file.
17
+
18
+ To read the full file, just provide file_path and leave from_line and to_line unset.
19
+
15
20
  Returns:
16
21
  str: File content with a header indicating the file name and line range. Example:
17
22
  - "---\nFile: /path/to/file.py | Lines: 1-10 (of 100)\n---\n<lines...>"
18
- - "---\nFile: /path/to/file.py | All lines (total: 100)\n---\n<all lines...>"
23
+ - "---\nFile: /path/to/file.py | All lines (total: 100 (all))\n---\n<all lines...>"
19
24
  - "Error reading file: <error message>"
20
25
  - "❗ not found"
21
26
  """
22
27
 
23
28
  def run(self, file_path: str, from_line: int = None, to_line: int = None) -> str:
24
- from janito.agent.tools.tools_utils import display_path
29
+ from janito.agent.tools_utils.utils import display_path
25
30
 
26
31
  disp_path = display_path(file_path)
27
32
  if from_line and to_line:
28
33
  self.report_info(
34
+ ActionType.READ,
29
35
  tr(
30
- "📖 Reading {disp_path} {from_line}-{to_line}",
36
+ "📖 Reading file '{disp_path}' {from_line}-{to_line}",
31
37
  disp_path=disp_path,
32
38
  from_line=from_line,
33
39
  to_line=to_line,
34
- )
40
+ ),
35
41
  )
36
42
  else:
37
- self.report_info(tr("📖 Reading {disp_path} all", disp_path=disp_path))
43
+ self.report_info(
44
+ ActionType.READ,
45
+ tr("📖 Reading file '{disp_path}'", disp_path=disp_path),
46
+ )
38
47
  try:
39
48
  with open(file_path, "r", encoding="utf-8", errors="replace") as f:
40
49
  lines = f.readlines()
@@ -59,7 +68,7 @@ class GetLinesTool(ToolBase):
59
68
  elif to_line < total_lines:
60
69
  self.report_success(
61
70
  tr(
62
- " ✅ {selected_len} {line_word} ({remaining} to eof)",
71
+ " ✅ {selected_len} {line_word} ({remaining} to end)",
63
72
  selected_len=selected_len,
64
73
  line_word=pluralize("line", selected_len),
65
74
  remaining=total_lines - to_line,
@@ -68,7 +77,7 @@ class GetLinesTool(ToolBase):
68
77
  else:
69
78
  self.report_success(
70
79
  tr(
71
- " ✅ {selected_len} {line_word}",
80
+ " ✅ {selected_len} {line_word} (all)",
72
81
  selected_len=selected_len,
73
82
  line_word=pluralize("line", selected_len),
74
83
  )
@@ -98,7 +107,7 @@ class GetLinesTool(ToolBase):
98
107
  )
99
108
  else:
100
109
  header = tr(
101
- "---\n{disp_path} All lines (total: {total_lines})\n---\n",
110
+ "---\n{disp_path} All lines (total: {total_lines} (all))\n---\n",
102
111
  disp_path=disp_path,
103
112
  total_lines=total_lines,
104
113
  )
@@ -1,8 +1,11 @@
1
1
  import os
2
2
  import shutil
3
3
  from janito.agent.tool_registry import register_tool
4
- from janito.agent.tools.utils import expand_path, display_path
4
+
5
+ # from janito.agent.tools_utils.expand_path import expand_path
6
+ from janito.agent.tools_utils.utils import display_path
5
7
  from janito.agent.tool_base import ToolBase
8
+ from janito.agent.tools_utils.action_type import ActionType
6
9
  from janito.i18n import tr
7
10
 
8
11
 
@@ -29,8 +32,8 @@ class MoveFileTool(ToolBase):
29
32
  ) -> str:
30
33
  original_src = src_path
31
34
  original_dest = dest_path
32
- src = expand_path(src_path)
33
- dest = expand_path(dest_path)
35
+ src = src_path # Using src_path as is
36
+ dest = dest_path # Using dest_path as is
34
37
  disp_src = display_path(original_src)
35
38
  disp_dest = display_path(original_dest)
36
39
  backup_path = None
@@ -82,19 +85,17 @@ class MoveFileTool(ToolBase):
82
85
  )
83
86
  return tr("❌ Error removing destination before move: {error}", error=e)
84
87
  try:
85
- shutil.move(src, dest)
86
- self.report_success(
88
+ self.report_info(
89
+ ActionType.WRITE,
87
90
  tr(
88
- " Moved from '{disp_src}' to '{disp_dest}'",
91
+ "📝 Moving from '{disp_src}' to '{disp_dest}' ...",
89
92
  disp_src=disp_src,
90
93
  disp_dest=disp_dest,
91
- )
92
- )
93
- msg = tr(
94
- "✅ Successfully moved from '{disp_src}' to '{disp_dest}'.",
95
- disp_src=disp_src,
96
- disp_dest=disp_dest,
94
+ ),
97
95
  )
96
+ shutil.move(src, dest)
97
+ self.report_success(tr("✅ Move complete."))
98
+ msg = tr("✅ Move complete.")
98
99
  if backup_path:
99
100
  msg += tr(
100
101
  " (backup at {backup_disp})",
@@ -1,5 +1,6 @@
1
1
  from typing import List
2
2
  from janito.agent.tool_base import ToolBase
3
+ from janito.agent.tools_utils.action_type import ActionType
3
4
  from janito.agent.tool_registry import register_tool
4
5
  from janito.i18n import tr
5
6
  import questionary
@@ -36,11 +37,12 @@ class PresentChoicesTool(ToolBase):
36
37
  if not choices:
37
38
  return tr("⚠️ No choices provided.")
38
39
  self.report_info(
40
+ ActionType.EXECUTE,
39
41
  tr(
40
42
  "ℹ️ Prompting user: {prompt} (multi_select={multi_select}) ...",
41
43
  prompt=prompt,
42
44
  multi_select=multi_select,
43
- )
45
+ ),
44
46
  )
45
47
  if multi_select:
46
48
  result = questionary.checkbox(
@@ -0,0 +1,150 @@
1
+ import subprocess
2
+ import sys
3
+ import tempfile
4
+ import threading
5
+ from janito.agent.tool_base import ToolBase
6
+ from janito.agent.tools_utils.action_type import ActionType
7
+ from janito.agent.tool_registry import register_tool
8
+ from janito.i18n import tr
9
+
10
+
11
+ @register_tool(name="python_command_runner")
12
+ class PythonCommandRunnerTool(ToolBase):
13
+ """
14
+ Tool to execute Python code using the `python -c` command-line flag.
15
+ Args:
16
+ code (str): The Python code to execute as a string.
17
+ timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
18
+ Returns:
19
+ str: Output and status message, or file paths/line counts if output is large.
20
+ """
21
+
22
+ def run(self, code: str, timeout: int = 60) -> str:
23
+ if not code.strip():
24
+ self.report_warning(tr("\u2139\ufe0f Empty code provided."))
25
+ return tr("Warning: Empty code provided. Operation skipped.")
26
+ self.report_info(
27
+ ActionType.EXECUTE, tr("🐍 Running: python -c ...\n{code}\n", code=code)
28
+ )
29
+ try:
30
+ with (
31
+ tempfile.NamedTemporaryFile(
32
+ mode="w+",
33
+ prefix="python_cmd_stdout_",
34
+ delete=False,
35
+ encoding="utf-8",
36
+ ) as stdout_file,
37
+ tempfile.NamedTemporaryFile(
38
+ mode="w+",
39
+ prefix="python_cmd_stderr_",
40
+ delete=False,
41
+ encoding="utf-8",
42
+ ) as stderr_file,
43
+ ):
44
+ process = subprocess.Popen(
45
+ [sys.executable, "-c", code],
46
+ stdout=subprocess.PIPE,
47
+ stderr=subprocess.PIPE,
48
+ text=True,
49
+ bufsize=1,
50
+ universal_newlines=True,
51
+ encoding="utf-8",
52
+ env={**dict(), **dict(PYTHONIOENCODING="utf-8")},
53
+ )
54
+ stdout_lines = 0
55
+ stderr_lines = 0
56
+
57
+ def stream_output(stream, file_obj, report_func, count_func):
58
+ nonlocal stdout_lines, stderr_lines
59
+ for line in stream:
60
+ file_obj.write(line)
61
+ file_obj.flush()
62
+ report_func(line)
63
+ if count_func == "stdout":
64
+ stdout_lines += 1
65
+ else:
66
+ stderr_lines += 1
67
+
68
+ stdout_thread = threading.Thread(
69
+ target=stream_output,
70
+ args=(process.stdout, stdout_file, self.report_stdout, "stdout"),
71
+ )
72
+ stderr_thread = threading.Thread(
73
+ target=stream_output,
74
+ args=(process.stderr, stderr_file, self.report_stderr, "stderr"),
75
+ )
76
+ stdout_thread.start()
77
+ stderr_thread.start()
78
+ try:
79
+ return_code = process.wait(timeout=timeout)
80
+ except subprocess.TimeoutExpired:
81
+ process.kill()
82
+ self.report_error(
83
+ tr("\u274c Timed out after {timeout} seconds.", timeout=timeout)
84
+ )
85
+ return tr(
86
+ "Code timed out after {timeout} seconds.", timeout=timeout
87
+ )
88
+ stdout_thread.join()
89
+ stderr_thread.join()
90
+ stdout_file.flush()
91
+ stderr_file.flush()
92
+
93
+ self.report_success(
94
+ tr("\u2705 Return code {return_code}", return_code=return_code)
95
+ )
96
+ # Read back the content for summary if not too large
97
+ with open(
98
+ stdout_file.name, "r", encoding="utf-8", errors="replace"
99
+ ) as out_f:
100
+ stdout_content = out_f.read()
101
+ with open(
102
+ stderr_file.name, "r", encoding="utf-8", errors="replace"
103
+ ) as err_f:
104
+ stderr_content = err_f.read()
105
+ max_lines = 100
106
+ stdout_lines = stdout_content.count("\n")
107
+ stderr_lines = stderr_content.count("\n")
108
+
109
+ def head_tail(text, n=10):
110
+ lines = text.splitlines()
111
+ if len(lines) <= 2 * n:
112
+ return "\n".join(lines)
113
+ return "\n".join(
114
+ lines[:n]
115
+ + ["... ({} lines omitted) ...".format(len(lines) - 2 * n)]
116
+ + lines[-n:]
117
+ )
118
+
119
+ if stdout_lines <= max_lines and stderr_lines <= max_lines:
120
+ result = (
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
+ f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
129
+ )
130
+ if stderr_lines > 0 and stderr_content.strip():
131
+ result += (
132
+ f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
133
+ )
134
+ result += f"returncode: {return_code}\n"
135
+ result += (
136
+ "--- STDOUT (head/tail) ---\n"
137
+ + head_tail(stdout_content)
138
+ + "\n"
139
+ )
140
+ if stderr_content.strip():
141
+ result += (
142
+ "--- STDERR (head/tail) ---\n"
143
+ + head_tail(stderr_content)
144
+ + "\n"
145
+ )
146
+ result += "Use the get_lines tool to inspect the contents of these files when needed."
147
+ return result
148
+ except Exception as e:
149
+ self.report_error(tr("\u274c Error: {error}", error=e))
150
+ return tr("Error running code: {error}", error=e)
@@ -0,0 +1,148 @@
1
+ import subprocess
2
+ import sys
3
+ import tempfile
4
+ import threading
5
+ from janito.agent.tool_base import ToolBase
6
+ from janito.agent.tools_utils.action_type import ActionType
7
+ from janito.agent.tool_registry import register_tool
8
+ from janito.i18n import tr
9
+
10
+
11
+ @register_tool(name="python_file_runner")
12
+ class PythonFileRunnerTool(ToolBase):
13
+ """
14
+ Tool to execute a specified Python script file.
15
+ Args:
16
+ file_path (str): Path to the Python script file to execute.
17
+ timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
18
+ Returns:
19
+ str: Output and status message, or file paths/line counts if output is large.
20
+ """
21
+
22
+ def run(self, file_path: str, timeout: int = 60) -> str:
23
+ self.report_info(
24
+ ActionType.EXECUTE,
25
+ tr("🚀 Running: python {file_path}", file_path=file_path),
26
+ )
27
+ try:
28
+ with (
29
+ tempfile.NamedTemporaryFile(
30
+ mode="w+",
31
+ prefix="python_file_stdout_",
32
+ delete=False,
33
+ encoding="utf-8",
34
+ ) as stdout_file,
35
+ tempfile.NamedTemporaryFile(
36
+ mode="w+",
37
+ prefix="python_file_stderr_",
38
+ delete=False,
39
+ encoding="utf-8",
40
+ ) as stderr_file,
41
+ ):
42
+ process = subprocess.Popen(
43
+ [sys.executable, file_path],
44
+ stdout=subprocess.PIPE,
45
+ stderr=subprocess.PIPE,
46
+ text=True,
47
+ bufsize=1,
48
+ universal_newlines=True,
49
+ encoding="utf-8",
50
+ env={**dict(), **dict(PYTHONIOENCODING="utf-8")},
51
+ )
52
+ stdout_lines = 0
53
+ stderr_lines = 0
54
+
55
+ def stream_output(stream, file_obj, report_func, count_func):
56
+ nonlocal stdout_lines, stderr_lines
57
+ for line in stream:
58
+ file_obj.write(line)
59
+ file_obj.flush()
60
+ report_func(line)
61
+ if count_func == "stdout":
62
+ stdout_lines += 1
63
+ else:
64
+ stderr_lines += 1
65
+
66
+ stdout_thread = threading.Thread(
67
+ target=stream_output,
68
+ args=(process.stdout, stdout_file, self.report_stdout, "stdout"),
69
+ )
70
+ stderr_thread = threading.Thread(
71
+ target=stream_output,
72
+ args=(process.stderr, stderr_file, self.report_stderr, "stderr"),
73
+ )
74
+ stdout_thread.start()
75
+ stderr_thread.start()
76
+ try:
77
+ return_code = process.wait(timeout=timeout)
78
+ except subprocess.TimeoutExpired:
79
+ process.kill()
80
+ self.report_error(
81
+ tr("\u274c Timed out after {timeout} seconds.", timeout=timeout)
82
+ )
83
+ return tr(
84
+ "Code timed out after {timeout} seconds.", timeout=timeout
85
+ )
86
+ stdout_thread.join()
87
+ stderr_thread.join()
88
+ stdout_file.flush()
89
+ stderr_file.flush()
90
+
91
+ self.report_success(
92
+ tr("\u2705 Return code {return_code}", return_code=return_code)
93
+ )
94
+ # Read back the content for summary if not too large
95
+ with open(
96
+ stdout_file.name, "r", encoding="utf-8", errors="replace"
97
+ ) as out_f:
98
+ stdout_content = out_f.read()
99
+ with open(
100
+ stderr_file.name, "r", encoding="utf-8", errors="replace"
101
+ ) as err_f:
102
+ stderr_content = err_f.read()
103
+ max_lines = 100
104
+ stdout_lines = stdout_content.count("\n")
105
+ stderr_lines = stderr_content.count("\n")
106
+
107
+ def head_tail(text, n=10):
108
+ lines = text.splitlines()
109
+ if len(lines) <= 2 * n:
110
+ return "\n".join(lines)
111
+ return "\n".join(
112
+ lines[:n]
113
+ + ["... ({} lines omitted) ...".format(len(lines) - 2 * n)]
114
+ + lines[-n:]
115
+ )
116
+
117
+ if stdout_lines <= max_lines and stderr_lines <= max_lines:
118
+ result = (
119
+ f"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}"
120
+ )
121
+ if stderr_content.strip():
122
+ result += f"\n--- STDERR ---\n{stderr_content}"
123
+ return result
124
+ else:
125
+ result = (
126
+ f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
127
+ )
128
+ if stderr_lines > 0 and stderr_content.strip():
129
+ result += (
130
+ f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
131
+ )
132
+ result += f"returncode: {return_code}\n"
133
+ result += (
134
+ "--- STDOUT (head/tail) ---\n"
135
+ + head_tail(stdout_content)
136
+ + "\n"
137
+ )
138
+ if stderr_content.strip():
139
+ result += (
140
+ "--- STDERR (head/tail) ---\n"
141
+ + head_tail(stderr_content)
142
+ + "\n"
143
+ )
144
+ result += "Use the get_lines tool to inspect the contents of these files when needed."
145
+ return result
146
+ except Exception as e:
147
+ self.report_error(tr("\u274c Error: {error}", error=e))
148
+ return tr("Error running file: {error}", error=e)
@@ -0,0 +1,154 @@
1
+ import subprocess
2
+ import sys
3
+ import tempfile
4
+ import threading
5
+ from janito.agent.tool_base import ToolBase
6
+ from janito.agent.tools_utils.action_type import ActionType
7
+ from janito.agent.tool_registry import register_tool
8
+ from janito.i18n import tr
9
+
10
+
11
+ @register_tool(name="python_stdin_runner")
12
+ class PythonStdinRunnerTool(ToolBase):
13
+ """
14
+ Tool to execute Python code by passing it to the interpreter via standard input (stdin).
15
+ Args:
16
+ code (str): The Python code to execute as a string.
17
+ timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
18
+ Returns:
19
+ str: Output and status message, or file paths/line counts if output is large.
20
+ """
21
+
22
+ def run(self, code: str, timeout: int = 60) -> str:
23
+ if not code.strip():
24
+ self.report_warning(tr("\u2139\ufe0f Empty code provided."))
25
+ return tr("Warning: Empty code provided. Operation skipped.")
26
+ self.report_info(
27
+ ActionType.EXECUTE,
28
+ tr("⚡ Running: python (stdin mode) ...\n{code}\n", code=code),
29
+ )
30
+ try:
31
+ with (
32
+ tempfile.NamedTemporaryFile(
33
+ mode="w+",
34
+ prefix="python_stdin_stdout_",
35
+ delete=False,
36
+ encoding="utf-8",
37
+ ) as stdout_file,
38
+ tempfile.NamedTemporaryFile(
39
+ mode="w+",
40
+ prefix="python_stdin_stderr_",
41
+ delete=False,
42
+ encoding="utf-8",
43
+ ) as stderr_file,
44
+ ):
45
+ process = subprocess.Popen(
46
+ [sys.executable],
47
+ stdin=subprocess.PIPE,
48
+ stdout=subprocess.PIPE,
49
+ stderr=subprocess.PIPE,
50
+ text=True,
51
+ bufsize=1,
52
+ universal_newlines=True,
53
+ encoding="utf-8",
54
+ env={**dict(), **dict(PYTHONIOENCODING="utf-8")},
55
+ )
56
+ stdout_lines = 0
57
+ stderr_lines = 0
58
+
59
+ def stream_output(stream, file_obj, report_func, count_func):
60
+ nonlocal stdout_lines, stderr_lines
61
+ for line in stream:
62
+ file_obj.write(line)
63
+ file_obj.flush()
64
+ report_func(line)
65
+ if count_func == "stdout":
66
+ stdout_lines += 1
67
+ else:
68
+ stderr_lines += 1
69
+
70
+ stdout_thread = threading.Thread(
71
+ target=stream_output,
72
+ args=(process.stdout, stdout_file, self.report_stdout, "stdout"),
73
+ )
74
+ stderr_thread = threading.Thread(
75
+ target=stream_output,
76
+ args=(process.stderr, stderr_file, self.report_stderr, "stderr"),
77
+ )
78
+ stdout_thread.start()
79
+ stderr_thread.start()
80
+ try:
81
+ process.stdin.write(code)
82
+ process.stdin.close()
83
+ return_code = process.wait(timeout=timeout)
84
+ except subprocess.TimeoutExpired:
85
+ process.kill()
86
+ self.report_error(
87
+ tr("\u274c Timed out after {timeout} seconds.", timeout=timeout)
88
+ )
89
+ return tr(
90
+ "Code timed out after {timeout} seconds.", timeout=timeout
91
+ )
92
+ stdout_thread.join()
93
+ stderr_thread.join()
94
+ stdout_file.flush()
95
+ stderr_file.flush()
96
+
97
+ self.report_success(
98
+ tr("\u2705 Return code {return_code}", return_code=return_code)
99
+ )
100
+ # Read back the content for summary if not too large
101
+ with open(
102
+ stdout_file.name, "r", encoding="utf-8", errors="replace"
103
+ ) as out_f:
104
+ stdout_content = out_f.read()
105
+ with open(
106
+ stderr_file.name, "r", encoding="utf-8", errors="replace"
107
+ ) as err_f:
108
+ stderr_content = err_f.read()
109
+ max_lines = 100
110
+ stdout_lines = stdout_content.count("\n")
111
+ stderr_lines = stderr_content.count("\n")
112
+
113
+ def head_tail(text, n=10):
114
+ lines = text.splitlines()
115
+ if len(lines) <= 2 * n:
116
+ return "\n".join(lines)
117
+ return "\n".join(
118
+ lines[:n]
119
+ + ["... ({} lines omitted) ...".format(len(lines) - 2 * n)]
120
+ + lines[-n:]
121
+ )
122
+
123
+ if stdout_lines <= max_lines and stderr_lines <= max_lines:
124
+ result = (
125
+ f"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}"
126
+ )
127
+ if stderr_content.strip():
128
+ result += f"\n--- STDERR ---\n{stderr_content}"
129
+ return result
130
+ else:
131
+ result = (
132
+ f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
133
+ )
134
+ if stderr_lines > 0 and stderr_content.strip():
135
+ result += (
136
+ f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
137
+ )
138
+ result += f"returncode: {return_code}\n"
139
+ result += (
140
+ "--- STDOUT (head/tail) ---\n"
141
+ + head_tail(stdout_content)
142
+ + "\n"
143
+ )
144
+ if stderr_content.strip():
145
+ result += (
146
+ "--- STDERR (head/tail) ---\n"
147
+ + head_tail(stderr_content)
148
+ + "\n"
149
+ )
150
+ result += "Use the get_lines tool to inspect the contents of these files when needed."
151
+ return result
152
+ except Exception as e:
153
+ self.report_error(tr("\u274c Error: {error}", error=e))
154
+ return tr("Error running code via stdin: {error}", error=e)
@@ -1,6 +1,7 @@
1
1
  from janito.agent.tool_base import ToolBase
2
+ from janito.agent.tools_utils.action_type import ActionType
2
3
  from janito.agent.tool_registry import register_tool
3
- from janito.agent.tools.tools_utils import pluralize, display_path
4
+ from janito.agent.tools_utils.utils import pluralize, display_path
4
5
  from janito.i18n import tr
5
6
  import shutil
6
7
  import os
@@ -24,7 +25,8 @@ class RemoveDirectoryTool(ToolBase):
24
25
  def run(self, file_path: str, recursive: bool = False) -> str:
25
26
  disp_path = display_path(file_path)
26
27
  self.report_info(
27
- tr("🗃️ Removing directory: {disp_path} ...", disp_path=disp_path)
28
+ ActionType.WRITE,
29
+ tr("🗃️ Removing directory '{disp_path}' ...", disp_path=disp_path),
28
30
  )
29
31
  backup_zip = None
30
32
  try: