janito 1.7.0__py3-none-any.whl → 1.8.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 (115) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/config.py +1 -1
  3. janito/agent/config_defaults.py +2 -2
  4. janito/agent/conversation.py +70 -27
  5. janito/agent/conversation_api.py +104 -4
  6. janito/agent/conversation_exceptions.py +6 -0
  7. janito/agent/conversation_tool_calls.py +17 -3
  8. janito/agent/event.py +24 -0
  9. janito/agent/event_dispatcher.py +24 -0
  10. janito/agent/event_handler_protocol.py +5 -0
  11. janito/agent/event_system.py +15 -0
  12. janito/agent/message_handler.py +4 -1
  13. janito/agent/message_handler_protocol.py +5 -0
  14. janito/agent/openai_client.py +5 -8
  15. janito/agent/openai_schema_generator.py +23 -4
  16. janito/agent/profile_manager.py +15 -83
  17. janito/agent/queued_message_handler.py +22 -3
  18. janito/agent/rich_message_handler.py +66 -72
  19. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +14 -0
  20. janito/agent/templates/profiles/system_prompt_template_base_pt.txt.j2 +13 -0
  21. janito/agent/test_handler_protocols.py +47 -0
  22. janito/agent/tests/__init__.py +1 -0
  23. janito/agent/tool_base.py +1 -1
  24. janito/agent/tool_executor.py +109 -0
  25. janito/agent/tool_registry.py +3 -75
  26. janito/agent/tool_use_tracker.py +46 -0
  27. janito/agent/tools/__init__.py +8 -9
  28. janito/agent/tools/ask_user.py +19 -11
  29. janito/agent/tools/create_directory.py +43 -28
  30. janito/agent/tools/create_file.py +60 -29
  31. janito/agent/tools/dir_walk_utils.py +16 -0
  32. janito/agent/tools/fetch_url.py +10 -11
  33. janito/agent/tools/find_files.py +49 -32
  34. janito/agent/tools/get_lines.py +54 -18
  35. janito/agent/tools/memory.py +32 -52
  36. janito/agent/tools/move_file.py +72 -23
  37. janito/agent/tools/outline_file/__init__.py +85 -0
  38. janito/agent/tools/outline_file/formatting.py +20 -0
  39. janito/agent/tools/outline_file/markdown_outline.py +14 -0
  40. janito/agent/tools/outline_file/python_outline.py +71 -0
  41. janito/agent/tools/present_choices.py +62 -0
  42. janito/agent/tools/present_choices_test.py +18 -0
  43. janito/agent/tools/remove_directory.py +31 -26
  44. janito/agent/tools/remove_file.py +31 -13
  45. janito/agent/tools/replace_text_in_file.py +135 -36
  46. janito/agent/tools/run_bash_command.py +47 -50
  47. janito/agent/tools/run_powershell_command.py +52 -36
  48. janito/agent/tools/run_python_command.py +49 -29
  49. janito/agent/tools/search_outline.py +17 -0
  50. janito/agent/tools/search_text.py +208 -0
  51. janito/agent/tools/tools_utils.py +47 -4
  52. janito/agent/tools/utils.py +14 -15
  53. janito/agent/tools/validate_file_syntax.py +163 -0
  54. janito/cli/arg_parser.py +36 -4
  55. janito/cli/logging_setup.py +7 -2
  56. janito/cli/main.py +96 -2
  57. janito/cli/runner/_termweb_log_utils.py +17 -0
  58. janito/cli/runner/cli_main.py +119 -77
  59. janito/cli/runner/config.py +2 -2
  60. janito/cli/termweb_starter.py +73 -0
  61. janito/cli_chat_shell/chat_loop.py +42 -7
  62. janito/cli_chat_shell/chat_state.py +1 -1
  63. janito/cli_chat_shell/chat_ui.py +0 -1
  64. janito/cli_chat_shell/commands/__init__.py +15 -6
  65. janito/cli_chat_shell/commands/{history_reset.py → history_start.py} +13 -5
  66. janito/cli_chat_shell/commands/lang.py +16 -0
  67. janito/cli_chat_shell/commands/prompt.py +42 -0
  68. janito/cli_chat_shell/commands/session_control.py +36 -1
  69. janito/cli_chat_shell/commands/termweb_log.py +86 -0
  70. janito/cli_chat_shell/commands/utility.py +5 -2
  71. janito/cli_chat_shell/commands/verbose.py +29 -0
  72. janito/cli_chat_shell/session_manager.py +9 -1
  73. janito/cli_chat_shell/shell_command_completer.py +20 -0
  74. janito/cli_chat_shell/ui.py +110 -99
  75. janito/i18n/__init__.py +35 -0
  76. janito/i18n/messages.py +23 -0
  77. janito/i18n/pt.py +46 -0
  78. janito/rich_utils.py +43 -43
  79. janito/termweb/app.py +95 -0
  80. janito/termweb/static/editor.html +238 -0
  81. janito/termweb/static/editor.html.bak +238 -0
  82. janito/termweb/static/explorer.html.bak +59 -0
  83. janito/termweb/static/favicon.ico +0 -0
  84. janito/termweb/static/favicon.ico.bak +0 -0
  85. janito/termweb/static/index.html +55 -0
  86. janito/termweb/static/index.html.bak +55 -0
  87. janito/termweb/static/index.html.bak.bak +175 -0
  88. janito/termweb/static/landing.html.bak +36 -0
  89. janito/termweb/static/termicon.svg +1 -0
  90. janito/termweb/static/termweb.css +235 -0
  91. janito/termweb/static/termweb.css.bak +286 -0
  92. janito/termweb/static/termweb.js +187 -0
  93. janito/termweb/static/termweb.js.bak +187 -0
  94. janito/termweb/static/termweb.js.bak.bak +157 -0
  95. janito/termweb/static/termweb_quickopen.js +135 -0
  96. janito/termweb/static/termweb_quickopen.js.bak +125 -0
  97. janito/web/app.py +4 -4
  98. {janito-1.7.0.dist-info → janito-1.8.0.dist-info}/METADATA +58 -25
  99. janito-1.8.0.dist-info/RECORD +127 -0
  100. {janito-1.7.0.dist-info → janito-1.8.0.dist-info}/WHEEL +1 -1
  101. janito/agent/templates/profiles/system_prompt_template_base.toml +0 -76
  102. janito/agent/templates/profiles/system_prompt_template_default.toml +0 -3
  103. janito/agent/templates/profiles/system_prompt_template_technical.toml +0 -13
  104. janito/agent/tests/test_prompt_toml.py +0 -61
  105. janito/agent/tool_registry_core.py +0 -2
  106. janito/agent/tools/get_file_outline.py +0 -146
  107. janito/agent/tools/py_compile_file.py +0 -40
  108. janito/agent/tools/replace_file.py +0 -51
  109. janito/agent/tools/search_files.py +0 -65
  110. janito/cli/runner/scan.py +0 -57
  111. janito/cli_chat_shell/commands/system.py +0 -73
  112. janito-1.7.0.dist-info/RECORD +0 -89
  113. {janito-1.7.0.dist-info → janito-1.8.0.dist-info}/entry_points.txt +0 -0
  114. {janito-1.7.0.dist-info → janito-1.8.0.dist-info}/licenses/LICENSE +0 -0
  115. {janito-1.7.0.dist-info → janito-1.8.0.dist-info}/top_level.txt +0 -0
@@ -1,68 +1,48 @@
1
- """
2
- In-memory memory tools for storing and retrieving reusable information during an agent session.
3
- These tools allow the agent to remember and recall arbitrary key-value pairs for the duration of the process.
4
- """
5
-
6
1
  from janito.agent.tool_base import ToolBase
7
2
  from janito.agent.tool_registry import register_tool
8
-
9
- # Simple in-memory store (process-local, not persistent)
10
- _memory_store = {}
3
+ from janito.i18n import tr
11
4
 
12
5
 
13
- @register_tool(name="store_memory")
14
- class StoreMemoryTool(ToolBase):
6
+ @register_tool(name="memory")
7
+ class MemoryTool(ToolBase):
15
8
  """
16
- Store a value for later retrieval using a key. Use this tool to remember information that may be useful in future steps or requests.
17
-
18
- Args:
19
- key (str): The identifier for the value to store.
20
- value (str): The value to store for later retrieval.
21
- Returns:
22
- str: Status message indicating success or error. Example:
23
- - "✅ Stored value for key: 'foo'"
24
- - "❗ Error storing value: ..."
9
+ Simple in-memory key-value store for demonstration purposes.
25
10
  """
26
11
 
27
- def call(self, key: str, value: str) -> str:
28
- self.report_info(f"Storing value for key: '{key}'")
29
- try:
30
- _memory_store[key] = value
31
- msg = f"✅ Stored value for key: '{key}'"
12
+ def __init__(self):
13
+ super().__init__()
14
+ self.memory = {}
15
+
16
+ def run(self, action: str, key: str, value: str = None) -> str:
17
+ if action == "set":
18
+ self.report_info(tr("ℹ️ Storing value for key: '{key}' ...", key=key))
19
+ self.memory[key] = value
20
+ msg = tr("Value stored for key: '{key}'.", key=key)
32
21
  self.report_success(msg)
33
22
  return msg
34
- except Exception as e:
35
- msg = f"❗ Error storing value: {e}"
36
- self.report_error(msg)
37
- return msg
38
-
39
-
40
- @register_tool(name="retrieve_memory")
41
- class RetrieveMemoryTool(ToolBase):
42
- """
43
- Retrieve a value previously stored using a key. Use this tool to recall information remembered earlier in the session.
44
-
45
- Args:
46
- key (str): The identifier for the value to retrieve.
47
- Returns:
48
- str: The stored value, or a warning message if not found. Example:
49
- - "🔎 Retrieved value for key: 'foo': bar"
50
- - "⚠️ No value found for key: 'notfound'"
51
- """
52
-
53
- def call(self, key: str) -> str:
54
- self.report_info(f"Retrieving value for key: '{key}'")
55
- try:
56
- if key in _memory_store:
57
- value = _memory_store[key]
58
- msg = f"🔎 Retrieved value for key: '{key}': {value}"
23
+ elif action == "get":
24
+ self.report_info(tr("ℹ️ Retrieving value for key: '{key}' ...", key=key))
25
+ if key in self.memory:
26
+ msg = tr(
27
+ "Value for key '{key}': {value}", key=key, value=self.memory[key]
28
+ )
59
29
  self.report_success(msg)
60
30
  return msg
61
31
  else:
62
- msg = f"⚠️ No value found for key: '{key}'"
32
+ msg = tr("Key '{key}' not found.", key=key)
63
33
  self.report_warning(msg)
64
34
  return msg
65
- except Exception as e:
66
- msg = f"❗ Error retrieving value: {e}"
35
+ elif action == "delete":
36
+ if key in self.memory:
37
+ del self.memory[key]
38
+ msg = tr("Key '{key}' deleted.", key=key)
39
+ self.report_success(msg)
40
+ return msg
41
+ else:
42
+ msg = tr("Key '{key}' not found.", key=key)
43
+ self.report_error(msg)
44
+ return msg
45
+ else:
46
+ msg = tr("Unknown action: {action}", action=action)
67
47
  self.report_error(msg)
68
48
  return msg
@@ -3,23 +3,24 @@ import shutil
3
3
  from janito.agent.tool_registry import register_tool
4
4
  from janito.agent.tools.utils import expand_path, display_path
5
5
  from janito.agent.tool_base import ToolBase
6
+ from janito.i18n import tr
6
7
 
7
8
 
8
9
  @register_tool(name="move_file")
9
10
  class MoveFileTool(ToolBase):
10
11
  """
11
- Move a file from src_path to dest_path.
12
+ Move a file or directory from src_path to dest_path.
12
13
 
13
14
  Args:
14
- src_path (str): Source file path.
15
- dest_path (str): Destination file path.
15
+ src_path (str): Source file or directory path.
16
+ dest_path (str): Destination file or directory path.
16
17
  overwrite (bool, optional): Whether to overwrite if the destination exists. Defaults to False.
17
- backup (bool, optional): If True, create a backup (.bak) of the destination before moving if it exists. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
18
+ backup (bool, optional): If True, create a backup (.bak for files, .bak.zip for directories) of the destination before moving if it exists. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
18
19
  Returns:
19
20
  str: Status message indicating the result.
20
21
  """
21
22
 
22
- def call(
23
+ def run(
23
24
  self,
24
25
  src_path: str,
25
26
  dest_path: str,
@@ -30,30 +31,78 @@ class MoveFileTool(ToolBase):
30
31
  original_dest = dest_path
31
32
  src = expand_path(src_path)
32
33
  dest = expand_path(dest_path)
33
- disp_src = display_path(original_src, src)
34
- disp_dest = display_path(original_dest, dest)
35
-
34
+ disp_src = display_path(original_src)
35
+ disp_dest = display_path(original_dest)
36
+ backup_path = None
36
37
  if not os.path.exists(src):
37
- self.report_error(f"\u274c Source file '{disp_src}' does not exist.")
38
- return f"\u274c Source file '{disp_src}' does not exist."
39
- if not os.path.isfile(src):
40
- self.report_error(f"\u274c Source path '{disp_src}' is not a file.")
41
- return f"\u274c Source path '{disp_src}' is not a file."
38
+ self.report_error(
39
+ tr(" Source '{disp_src}' does not exist.", disp_src=disp_src)
40
+ )
41
+ return tr(" Source '{disp_src}' does not exist.", disp_src=disp_src)
42
+ is_src_file = os.path.isfile(src)
43
+ is_src_dir = os.path.isdir(src)
44
+ if not (is_src_file or is_src_dir):
45
+ self.report_error(
46
+ tr(
47
+ "❌ Source path '{disp_src}' is neither a file nor a directory.",
48
+ disp_src=disp_src,
49
+ )
50
+ )
51
+ return tr(
52
+ "❌ Source path '{disp_src}' is neither a file nor a directory.",
53
+ disp_src=disp_src,
54
+ )
42
55
  if os.path.exists(dest):
43
56
  if not overwrite:
44
57
  self.report_error(
45
- f"\u2757 Destination '{disp_dest}' exists and overwrite is False."
58
+ tr(
59
+ "❗ Destination '{disp_dest}' exists and overwrite is False.",
60
+ disp_dest=disp_dest,
61
+ )
62
+ )
63
+ return tr(
64
+ "❗ Destination '{disp_dest}' already exists and overwrite is False.",
65
+ disp_dest=disp_dest,
46
66
  )
47
- return f"\u2757 Destination '{disp_dest}' already exists and overwrite is False."
48
- if os.path.isdir(dest):
49
- self.report_error(f"\u274c Destination '{disp_dest}' is a directory.")
50
- return f"\u274c Destination '{disp_dest}' is a directory."
51
67
  if backup:
52
- shutil.copy2(dest, dest + ".bak")
68
+ if os.path.isfile(dest):
69
+ backup_path = dest + ".bak"
70
+ shutil.copy2(dest, backup_path)
71
+ elif os.path.isdir(dest):
72
+ backup_path = dest.rstrip("/\\") + ".bak.zip"
73
+ shutil.make_archive(dest.rstrip("/\\") + ".bak", "zip", dest)
74
+ try:
75
+ if os.path.isfile(dest):
76
+ os.remove(dest)
77
+ elif os.path.isdir(dest):
78
+ shutil.rmtree(dest)
79
+ except Exception as e:
80
+ self.report_error(
81
+ tr("❌ Error removing destination before move: {error}", error=e)
82
+ )
83
+ return tr("❌ Error removing destination before move: {error}", error=e)
53
84
  try:
54
85
  shutil.move(src, dest)
55
- self.report_success(f"\u2705 File moved from '{disp_src}' to '{disp_dest}'")
56
- return f"\u2705 Successfully moved the file from '{disp_src}' to '{disp_dest}'."
86
+ self.report_success(
87
+ tr(
88
+ "✅ Moved from '{disp_src}' to '{disp_dest}'",
89
+ disp_src=disp_src,
90
+ 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,
97
+ )
98
+ if backup_path:
99
+ msg += tr(
100
+ " (backup at {backup_disp})",
101
+ backup_disp=display_path(
102
+ original_dest + (".bak" if is_src_file else ".bak.zip")
103
+ ),
104
+ )
105
+ return msg
57
106
  except Exception as e:
58
- self.report_error(f"\u274c Error moving file: {e}")
59
- return f"\u274c Error moving file: {e}"
107
+ self.report_error(tr(" Error moving: {error}", error=e))
108
+ return tr(" Error moving: {error}", error=e)
@@ -0,0 +1,85 @@
1
+ from janito.agent.tool_registry import register_tool
2
+ from .python_outline import parse_python_outline
3
+ from .markdown_outline import parse_markdown_outline
4
+ from .formatting import format_outline_table, format_markdown_outline_table
5
+ import os
6
+ from janito.agent.tool_base import ToolBase
7
+ from janito.agent.tools.tools_utils import display_path
8
+ from janito.i18n import tr
9
+
10
+
11
+ @register_tool(name="outline_file")
12
+ class GetFileOutlineTool(ToolBase):
13
+ """
14
+ Get an outline of a file's structure. Supports Python and Markdown files.
15
+
16
+ Args:
17
+ file_path (str): Path to the file to outline.
18
+ """
19
+
20
+ def run(self, file_path: str) -> str:
21
+ try:
22
+ self.report_info(
23
+ tr(
24
+ "📄 Outlining file: '{disp_path}' ...",
25
+ disp_path=display_path(file_path),
26
+ )
27
+ )
28
+ ext = os.path.splitext(file_path)[1].lower()
29
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
30
+ lines = f.readlines()
31
+ if ext == ".py":
32
+ outline_items = parse_python_outline(lines)
33
+ outline_type = "python"
34
+ table = format_outline_table(outline_items)
35
+ self.report_success(
36
+ tr(
37
+ "✅ {count} items ({outline_type})",
38
+ count=len(outline_items),
39
+ outline_type=outline_type,
40
+ )
41
+ )
42
+ return (
43
+ tr(
44
+ "Outline: {count} items ({outline_type})\n",
45
+ count=len(outline_items),
46
+ outline_type=outline_type,
47
+ )
48
+ + table
49
+ )
50
+ elif ext == ".md":
51
+ outline_items = parse_markdown_outline(lines)
52
+ outline_type = "markdown"
53
+ table = format_markdown_outline_table(outline_items)
54
+ self.report_success(
55
+ tr(
56
+ "✅ {count} items ({outline_type})",
57
+ count=len(outline_items),
58
+ outline_type=outline_type,
59
+ )
60
+ )
61
+ return (
62
+ tr(
63
+ "Outline: {count} items ({outline_type})\n",
64
+ count=len(outline_items),
65
+ outline_type=outline_type,
66
+ )
67
+ + table
68
+ )
69
+ else:
70
+ outline_type = "default"
71
+ self.report_success(
72
+ tr(
73
+ "✅ {count} lines ({outline_type})",
74
+ count=len(lines),
75
+ outline_type=outline_type,
76
+ )
77
+ )
78
+ return tr(
79
+ "Outline: {count} lines ({outline_type})\nFile has {count} lines.",
80
+ count=len(lines),
81
+ outline_type=outline_type,
82
+ )
83
+ except Exception as e:
84
+ self.report_error(tr("❌ Error reading file: {error}", error=e))
85
+ return tr("Error reading file: {error}", error=e)
@@ -0,0 +1,20 @@
1
+ def format_outline_table(outline_items):
2
+ if not outline_items:
3
+ return "No classes, functions, or variables found."
4
+ header = "| Type | Name | Start | End | Parent |\n|---------|-------------|-------|-----|----------|"
5
+ rows = []
6
+ for item in outline_items:
7
+ rows.append(
8
+ f"| {item['type']:<7} | {item['name']:<11} | {item['start']:<5} | {item['end']:<3} | {item['parent']:<8} |"
9
+ )
10
+ return header + "\n" + "\n".join(rows)
11
+
12
+
13
+ def format_markdown_outline_table(outline_items):
14
+ if not outline_items:
15
+ return "No headers found."
16
+ header = "| Level | Header | Line |\n|-------|----------------------------------|------|"
17
+ rows = []
18
+ for item in outline_items:
19
+ rows.append(f"| {item['level']:<5} | {item['title']:<32} | {item['line']:<4} |")
20
+ return header + "\n" + "\n".join(rows)
@@ -0,0 +1,14 @@
1
+ import re
2
+ from typing import List
3
+
4
+
5
+ def parse_markdown_outline(lines: List[str]):
6
+ header_pat = re.compile(r"^(#+)\s+(.*)")
7
+ outline = []
8
+ for idx, line in enumerate(lines):
9
+ match = header_pat.match(line)
10
+ if match:
11
+ level = len(match.group(1))
12
+ title = match.group(2).strip()
13
+ outline.append({"level": level, "title": title, "line": idx + 1})
14
+ return outline
@@ -0,0 +1,71 @@
1
+ import re
2
+ from typing import List
3
+
4
+
5
+ def parse_python_outline(lines: List[str]):
6
+ class_pat = re.compile(r"^(\s*)class\s+(\w+)")
7
+ func_pat = re.compile(r"^(\s*)def\s+(\w+)")
8
+ assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
9
+ outline = []
10
+ stack = [] # (type, name, indent, start, parent)
11
+ for idx, line in enumerate(lines):
12
+ class_match = class_pat.match(line)
13
+ func_match = func_pat.match(line)
14
+ assign_match = assign_pat.match(line)
15
+ indent = len(line) - len(line.lstrip())
16
+ if class_match:
17
+ name = class_match.group(2)
18
+ parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
19
+ stack.append(("class", name, indent, idx + 1, parent))
20
+ elif func_match:
21
+ name = func_match.group(2)
22
+ parent = (
23
+ stack[-1][1]
24
+ if stack
25
+ and stack[-1][0] in ("class", "function")
26
+ and indent > stack[-1][2]
27
+ else ""
28
+ )
29
+ stack.append(("function", name, indent, idx + 1, parent))
30
+ elif assign_match and indent == 0:
31
+ var_name = assign_match.group(2)
32
+ var_type = "const" if var_name.isupper() else "var"
33
+ outline.append(
34
+ {
35
+ "type": var_type,
36
+ "name": var_name,
37
+ "start": idx + 1,
38
+ "end": idx + 1,
39
+ "parent": "",
40
+ }
41
+ )
42
+ while stack and indent < stack[-1][2]:
43
+ popped = stack.pop()
44
+ outline.append(
45
+ {
46
+ "type": (
47
+ popped[0]
48
+ if popped[0] != "function" or popped[3] == 1
49
+ else ("method" if popped[4] else "function")
50
+ ),
51
+ "name": popped[1],
52
+ "start": popped[3],
53
+ "end": idx,
54
+ "parent": popped[4],
55
+ }
56
+ )
57
+ for popped in stack:
58
+ outline.append(
59
+ {
60
+ "type": (
61
+ popped[0]
62
+ if popped[0] != "function" or popped[3] == 1
63
+ else ("method" if popped[4] else "function")
64
+ ),
65
+ "name": popped[1],
66
+ "start": popped[3],
67
+ "end": len(lines),
68
+ "parent": popped[4],
69
+ }
70
+ )
71
+ return outline
@@ -0,0 +1,62 @@
1
+ from typing import List
2
+ from janito.agent.tool_base import ToolBase
3
+ from janito.agent.tool_registry import register_tool
4
+ from janito.i18n import tr
5
+ import questionary
6
+ from questionary import Style
7
+
8
+ custom_style = Style(
9
+ [
10
+ ("pointer", "fg:#ffffff bg:#1976d2 bold"),
11
+ ("highlighted", "fg:#ffffff bg:#1565c0 bold"),
12
+ ("answer", "fg:#1976d2 bold"),
13
+ ("qmark", "fg:#1976d2 bold"),
14
+ ]
15
+ )
16
+ HAND_EMOJI = "🖐️" # 🖐️
17
+
18
+
19
+ @register_tool(name="present_choices")
20
+ class PresentChoicesTool(ToolBase):
21
+ """
22
+ Present a list of options to the user and return the selected option(s).
23
+
24
+ Args:
25
+ prompt (str): The prompt/question to display.
26
+ choices (List[str]): List of options to present. Use \n in option text for explicit line breaks if needed.
27
+ multi_select (bool): If True, allow multiple selections.
28
+ Returns:
29
+ str: The selected option(s) as a string, or a message if cancelled.
30
+ - For multi_select=True, returns each selection on a new line, each prefixed with '- '.
31
+ - For multi_select=False, returns the selected option as a string.
32
+ - If cancelled, returns 'No selection made.'
33
+ """
34
+
35
+ def run(self, prompt: str, choices: List[str], multi_select: bool = False) -> str:
36
+ if not choices:
37
+ return tr("⚠️ No choices provided.")
38
+ self.report_info(
39
+ tr(
40
+ "ℹ️ Prompting user: {prompt} (multi_select={multi_select}) ...",
41
+ prompt=prompt,
42
+ multi_select=multi_select,
43
+ )
44
+ )
45
+ if multi_select:
46
+ result = questionary.checkbox(
47
+ prompt, choices=choices, style=custom_style, pointer=HAND_EMOJI
48
+ ).ask()
49
+ if result is None:
50
+ return tr("No selection made.")
51
+ return (
52
+ "\n".join(f"- {item}" for item in result)
53
+ if isinstance(result, list)
54
+ else f"- {result}"
55
+ )
56
+ else:
57
+ result = questionary.select(
58
+ prompt, choices=choices, style=custom_style, pointer=HAND_EMOJI
59
+ ).ask()
60
+ if result is None:
61
+ return tr("No selection made.")
62
+ return str(result)
@@ -0,0 +1,18 @@
1
+ """
2
+ Test for present_choices tool.
3
+
4
+ Note: This test is for manual/interactive verification only, as prompt_toolkit dialogs require user interaction.
5
+ """
6
+
7
+ from present_choices import present_choices
8
+
9
+ if __name__ == "__main__":
10
+ prompt = "Select your favorite fruits:"
11
+ choices = ["Apple", "Banana", "Cherry", "Date"]
12
+ print("Single-select test:")
13
+ selected = present_choices(prompt, choices, multi_select=False)
14
+ print(f"Selected: {selected}")
15
+
16
+ print("\nMulti-select test:")
17
+ selected_multi = present_choices(prompt, choices, multi_select=True)
18
+ print(f"Selected: {selected_multi}")
@@ -1,7 +1,7 @@
1
1
  from janito.agent.tool_base import ToolBase
2
2
  from janito.agent.tool_registry import register_tool
3
- from janito.agent.tools.tools_utils import pluralize
4
-
3
+ from janito.agent.tools.tools_utils import pluralize, display_path
4
+ from janito.i18n import tr
5
5
  import shutil
6
6
  import os
7
7
  import zipfile
@@ -10,41 +10,46 @@ import zipfile
10
10
  @register_tool(name="remove_directory")
11
11
  class RemoveDirectoryTool(ToolBase):
12
12
  """
13
- Remove a directory. If recursive=False and directory not empty, raises error.
13
+ Remove a directory.
14
14
 
15
15
  Args:
16
- directory (str): Path to the directory to remove.
17
- recursive (bool, optional): Remove recursively if True. Defaults to False.
18
- backup (bool, optional): If True, create a backup (.bak.zip) before removing. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
16
+ file_path (str): Path to the directory to remove.
17
+ recursive (bool, optional): If True, remove non-empty directories recursively (with backup). If False, only remove empty directories. Defaults to False.
19
18
  Returns:
20
19
  str: Status message indicating result. Example:
21
20
  - "Directory removed: /path/to/dir"
22
21
  - "Error removing directory: <error message>"
23
22
  """
24
23
 
25
- def call(
26
- self, directory: str, recursive: bool = False, backup: bool = False
27
- ) -> str:
24
+ def run(self, file_path: str, recursive: bool = False) -> str:
25
+ disp_path = display_path(file_path)
28
26
  self.report_info(
29
- f"\U0001f5c3\ufe0f Removing directory: {directory} (recursive={recursive})"
27
+ tr("🗃️ Removing directory: {disp_path} ...", disp_path=disp_path)
30
28
  )
29
+ backup_zip = None
31
30
  try:
32
- if backup and os.path.exists(directory) and os.path.isdir(directory):
33
- backup_zip = directory.rstrip("/\\") + ".bak.zip"
34
- with zipfile.ZipFile(backup_zip, "w", zipfile.ZIP_DEFLATED) as zipf:
35
- for root, dirs, files in os.walk(directory):
36
- for file in files:
37
- abs_path = os.path.join(root, file)
38
- rel_path = os.path.relpath(
39
- abs_path, os.path.dirname(directory)
40
- )
41
- zipf.write(abs_path, rel_path)
42
31
  if recursive:
43
- shutil.rmtree(directory)
32
+ # Backup before recursive removal
33
+ if os.path.exists(file_path) and os.path.isdir(file_path):
34
+ backup_zip = file_path.rstrip("/\\") + ".bak.zip"
35
+ with zipfile.ZipFile(backup_zip, "w", zipfile.ZIP_DEFLATED) as zipf:
36
+ for root, dirs, files in os.walk(file_path):
37
+ for file in files:
38
+ abs_path = os.path.join(root, file)
39
+ rel_path = os.path.relpath(
40
+ abs_path, os.path.dirname(file_path)
41
+ )
42
+ zipf.write(abs_path, rel_path)
43
+ shutil.rmtree(file_path)
44
44
  else:
45
- os.rmdir(directory)
46
- self.report_success(f"\u2705 1 {pluralize('directory', 1)}")
47
- return f"Directory removed: {directory}"
45
+ os.rmdir(file_path)
46
+ self.report_success(
47
+ tr(" 1 {dir_word}", dir_word=pluralize("directory", 1))
48
+ )
49
+ msg = tr("Directory removed: {disp_path}", disp_path=disp_path)
50
+ if backup_zip:
51
+ msg += tr(" (backup at {backup_zip})", backup_zip=backup_zip)
52
+ return msg
48
53
  except Exception as e:
49
- self.report_error(f" \u274c Error removing directory: {e}")
50
- return f"Error removing directory: {e}"
54
+ self.report_error(tr(" Error removing directory: {error}", error=e))
55
+ return tr("Error removing directory: {error}", error=e)
@@ -3,6 +3,7 @@ import shutil
3
3
  from janito.agent.tool_registry import register_tool
4
4
  from janito.agent.tools.utils import expand_path, display_path
5
5
  from janito.agent.tool_base import ToolBase
6
+ from janito.i18n import tr
6
7
 
7
8
 
8
9
  @register_tool(name="remove_file")
@@ -15,26 +16,43 @@ class RemoveFileTool(ToolBase):
15
16
  backup (bool, optional): If True, create a backup (.bak) before removing. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
16
17
  Returns:
17
18
  str: Status message indicating the result. Example:
18
- - "\u2705 Successfully removed the file at ..."
19
- - "\u2757 Cannot remove file: ..."
19
+ - " Successfully removed the file at ..."
20
+ - " Cannot remove file: ..."
20
21
  """
21
22
 
22
- def call(self, file_path: str, backup: bool = False) -> str:
23
+ def run(self, file_path: str, backup: bool = False) -> str:
23
24
  original_path = file_path
24
25
  path = expand_path(file_path)
25
- disp_path = display_path(original_path, path)
26
+ disp_path = display_path(original_path)
27
+ backup_path = None
26
28
  if not os.path.exists(path):
27
- self.report_error(f"\u274c File '{disp_path}' does not exist.")
28
- return f"\u274c File '{disp_path}' does not exist."
29
+ self.report_error(
30
+ tr(" File '{disp_path}' does not exist.", disp_path=disp_path)
31
+ )
32
+ return tr("❌ File '{disp_path}' does not exist.", disp_path=disp_path)
29
33
  if not os.path.isfile(path):
30
- self.report_error(f"\u274c Path '{disp_path}' is not a file.")
31
- return f"\u274c Path '{disp_path}' is not a file."
34
+ self.report_error(
35
+ tr(" Path '{disp_path}' is not a file.", disp_path=disp_path)
36
+ )
37
+ return tr("❌ Path '{disp_path}' is not a file.", disp_path=disp_path)
32
38
  try:
33
39
  if backup:
34
- shutil.copy2(path, path + ".bak")
40
+ backup_path = path + ".bak"
41
+ shutil.copy2(path, backup_path)
35
42
  os.remove(path)
36
- self.report_success(f"\u2705 File removed: '{disp_path}'")
37
- return f"\u2705 Successfully removed the file at '{disp_path}'."
43
+ self.report_success(
44
+ tr(" File removed: '{disp_path}'", disp_path=disp_path)
45
+ )
46
+ msg = tr(
47
+ "✅ Successfully removed the file at '{disp_path}'.",
48
+ disp_path=disp_path,
49
+ )
50
+ if backup_path:
51
+ msg += tr(
52
+ " (backup at {backup_disp})",
53
+ backup_disp=display_path(original_path + ".bak"),
54
+ )
55
+ return msg
38
56
  except Exception as e:
39
- self.report_error(f"\u274c Error removing file: {e}")
40
- return f"\u274c Error removing file: {e}"
57
+ self.report_error(tr(" Error removing file: {error}", error=e))
58
+ return tr(" Error removing file: {error}", error=e)