janito 1.3.2__py3-none-any.whl → 1.4.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/agent/templates/system_instructions.j2 +4 -8
- janito/agent/tool_handler.py +62 -95
- janito/agent/tools/__init__.py +8 -11
- janito/agent/tools/ask_user.py +59 -62
- janito/agent/tools/fetch_url.py +27 -32
- janito/agent/tools/file_ops.py +102 -60
- janito/agent/tools/find_files.py +25 -52
- janito/agent/tools/get_file_outline.py +21 -0
- janito/agent/tools/get_lines.py +32 -56
- janito/agent/tools/gitignore_utils.py +4 -1
- janito/agent/tools/py_compile.py +19 -22
- janito/agent/tools/python_exec.py +27 -20
- janito/agent/tools/remove_directory.py +20 -34
- janito/agent/tools/replace_text_in_file.py +61 -61
- janito/agent/tools/rich_utils.py +6 -6
- janito/agent/tools/run_bash_command.py +66 -120
- janito/agent/tools/search_files.py +21 -47
- janito/agent/tools/tool_base.py +4 -2
- janito/agent/tools/utils.py +31 -0
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/METADATA +6 -6
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/RECORD +26 -24
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/WHEEL +0 -0
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/entry_points.txt +0 -0
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/top_level.txt +0 -0
janito/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "1.
|
1
|
+
__version__ = "1.4.0"
|
@@ -4,6 +4,7 @@ You are an assistant for a analysis and development tool that operates on files
|
|
4
4
|
directories using text-based operations.
|
5
5
|
|
6
6
|
Provide a concise plan before calling any tool.
|
7
|
+
Always execute the plan immediately after presenting it, unless the user requests otherwise.
|
7
8
|
|
8
9
|
<context>
|
9
10
|
Always review `README_structure.txt` before conducting file-specific searches.
|
@@ -12,25 +13,20 @@ Explore files that might be relevant to the current task.
|
|
12
13
|
</context>
|
13
14
|
|
14
15
|
<analysis>
|
15
|
-
|
16
|
+
In case of missing code or functions, look into the .bak files and check git diff/history for recent changes.
|
16
17
|
</analysis>
|
17
18
|
|
18
19
|
<editing>
|
19
20
|
If in doubt during editing, use the `ask_user` function to get additional information; otherwise, proceed and inform the user of the decision made.
|
20
21
|
|
21
22
|
When you need to make changes to a file, consider the following:
|
22
|
-
|
23
|
-
- Use the `edit_file` tool when you want to update or fix specific text fragments within a file without altering the rest of its content. It is preferred over full file replacement when:
|
24
|
-
- Only small, targeted changes are needed.
|
25
|
-
- You want to avoid the risk of accidentally overwriting unrelated content.
|
26
|
-
- The file is large, and rewriting the entire file would be inefficient.
|
27
|
-
- You want to preserve formatting, comments, or code structure outside the replaced text.
|
28
|
-
|
23
|
+
- It is preferred to replace exact text occurrences over file overwriting.
|
29
24
|
- When replacing files, review their current content before requesting the update.
|
30
25
|
- When reorganizing, moving files, or functions, search for references in other files that might need to be updated accordingly.
|
31
26
|
</editing>
|
32
27
|
|
33
28
|
<finishing>
|
29
|
+
- When asked to commit and no message is provided, check the git diff and summarize the changes in the commit message.
|
34
30
|
- Review the README content if there are user-exposed or public API changes.
|
35
31
|
- Update `README_structure.txt` considering discovered, created, or modified files.
|
36
32
|
</finishing>
|
janito/agent/tool_handler.py
CHANGED
@@ -1,102 +1,40 @@
|
|
1
1
|
import os
|
2
2
|
import json
|
3
3
|
import traceback
|
4
|
+
from janito.agent.tools.tool_base import ToolBase
|
4
5
|
|
5
6
|
class ToolHandler:
|
6
7
|
_tool_registry = {}
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
from typing import get_origin, get_args
|
13
|
-
|
14
|
-
name = func.__name__
|
15
|
-
description = func.__doc__ or ""
|
16
|
-
|
17
|
-
sig = inspect.signature(func)
|
18
|
-
params_schema = {
|
19
|
-
"type": "object",
|
20
|
-
"properties": {},
|
21
|
-
"required": []
|
22
|
-
}
|
23
|
-
|
24
|
-
for param_name, param in sig.parameters.items():
|
25
|
-
if param.annotation is param.empty:
|
26
|
-
raise TypeError(f"Parameter '{param_name}' in tool '{name}' is missing a type hint.")
|
27
|
-
param_type = param.annotation
|
28
|
-
schema = {}
|
29
|
-
|
30
|
-
# Handle typing.Optional, typing.List, typing.Literal, etc.
|
31
|
-
origin = get_origin(param_type)
|
32
|
-
args = get_args(param_type)
|
33
|
-
|
34
|
-
if origin is typing.Union and type(None) in args:
|
35
|
-
# Optional[...] type
|
36
|
-
main_type = args[0] if args[1] is type(None) else args[1]
|
37
|
-
origin = get_origin(main_type)
|
38
|
-
args = get_args(main_type)
|
39
|
-
param_type = main_type
|
40
|
-
else:
|
41
|
-
main_type = param_type
|
42
|
-
|
43
|
-
if origin is list or origin is typing.List:
|
44
|
-
item_type = args[0] if args else str
|
45
|
-
item_schema = {"type": _pytype_to_json_type(item_type)}
|
46
|
-
schema = {"type": "array", "items": item_schema}
|
47
|
-
elif origin is typing.Literal:
|
48
|
-
schema = {"type": _pytype_to_json_type(type(args[0])), "enum": list(args)}
|
49
|
-
elif main_type == int:
|
50
|
-
schema = {"type": "integer"}
|
51
|
-
elif main_type == float:
|
52
|
-
schema = {"type": "number"}
|
53
|
-
elif main_type == bool:
|
54
|
-
schema = {"type": "boolean"}
|
55
|
-
elif main_type == dict:
|
56
|
-
schema = {"type": "object"}
|
57
|
-
elif main_type == list:
|
58
|
-
schema = {"type": "array", "items": {"type": "string"}}
|
59
|
-
else:
|
60
|
-
schema = {"type": "string"}
|
61
|
-
|
62
|
-
# Optionally add description if available in docstring (not implemented here)
|
63
|
-
params_schema["properties"][param_name] = schema
|
64
|
-
if param.default is param.empty:
|
65
|
-
params_schema["required"].append(param_name)
|
66
|
-
|
67
|
-
cls._tool_registry[name] = {
|
68
|
-
"function": func,
|
69
|
-
"description": description,
|
70
|
-
"parameters": params_schema
|
71
|
-
}
|
72
|
-
return func
|
73
|
-
|
74
|
-
def _pytype_to_json_type(pytype):
|
75
|
-
import typing
|
76
|
-
if pytype == int:
|
77
|
-
return "integer"
|
78
|
-
elif pytype == float:
|
79
|
-
return "number"
|
80
|
-
elif pytype == bool:
|
81
|
-
return "boolean"
|
82
|
-
elif pytype == dict:
|
83
|
-
return "object"
|
84
|
-
elif pytype == list or pytype == typing.List:
|
85
|
-
return "array"
|
86
|
-
else:
|
87
|
-
return "string"
|
88
|
-
|
89
|
-
class ToolHandler:
|
90
|
-
_tool_registry = {}
|
9
|
+
def __init__(self, verbose=False, enable_tools=True):
|
10
|
+
self.verbose = verbose
|
11
|
+
self.tools = []
|
12
|
+
self.enable_tools = enable_tools
|
91
13
|
|
92
14
|
@classmethod
|
93
|
-
def register_tool(cls,
|
15
|
+
def register_tool(cls, tool=None, *, name: str = None):
|
16
|
+
"""
|
17
|
+
Register a tool class derived from ToolBase.
|
18
|
+
Args:
|
19
|
+
tool: The tool class (must inherit from ToolBase).
|
20
|
+
name: Optional override for the tool name.
|
21
|
+
Raises:
|
22
|
+
TypeError: If the tool is not a subclass of ToolBase.
|
23
|
+
"""
|
24
|
+
if tool is None:
|
25
|
+
return lambda t: cls.register_tool(t, name=name)
|
94
26
|
import inspect
|
95
27
|
import typing
|
96
28
|
from typing import get_origin, get_args
|
97
29
|
|
98
|
-
|
99
|
-
|
30
|
+
override_name = name
|
31
|
+
if not (isinstance(tool, type) and issubclass(tool, ToolBase)):
|
32
|
+
raise TypeError("Tool must be a class derived from ToolBase.")
|
33
|
+
instance = tool()
|
34
|
+
func = instance.call
|
35
|
+
default_name = tool.__name__
|
36
|
+
name = override_name or default_name
|
37
|
+
description = tool.__doc__ or func.__doc__ or ""
|
100
38
|
|
101
39
|
sig = inspect.signature(func)
|
102
40
|
params_schema = {
|
@@ -143,22 +81,30 @@ class ToolHandler:
|
|
143
81
|
else:
|
144
82
|
schema = {"type": "string"}
|
145
83
|
|
146
|
-
#
|
84
|
+
# Add description from call method docstring if available (Google-style Args parsing)
|
85
|
+
if func.__doc__:
|
86
|
+
import re
|
87
|
+
doc = func.__doc__
|
88
|
+
args_section = re.search(r"Args:\s*(.*?)(?:\n\s*\w|Returns:|$)", doc, re.DOTALL)
|
89
|
+
param_descs = {}
|
90
|
+
if args_section:
|
91
|
+
args_text = args_section.group(1)
|
92
|
+
for match in re.finditer(r"(\w+) \([^)]+\): ([^\n]+)", args_text):
|
93
|
+
pname, pdesc = match.groups()
|
94
|
+
param_descs[pname] = pdesc.strip()
|
95
|
+
if param_name in param_descs:
|
96
|
+
schema["description"] = param_descs[param_name]
|
147
97
|
params_schema["properties"][param_name] = schema
|
148
98
|
if param.default is param.empty:
|
149
99
|
params_schema["required"].append(param_name)
|
150
100
|
|
101
|
+
# register the bound call function
|
151
102
|
cls._tool_registry[name] = {
|
152
103
|
"function": func,
|
153
104
|
"description": description,
|
154
105
|
"parameters": params_schema
|
155
106
|
}
|
156
|
-
return
|
157
|
-
|
158
|
-
def __init__(self, verbose=False, enable_tools=True):
|
159
|
-
self.verbose = verbose
|
160
|
-
self.tools = []
|
161
|
-
self.enable_tools = enable_tools
|
107
|
+
return tool
|
162
108
|
|
163
109
|
def register(self, func):
|
164
110
|
self.tools.append(func)
|
@@ -194,6 +140,11 @@ class ToolHandler:
|
|
194
140
|
print(f"[Tool Call] {tool_call.function.name} called with arguments: {args}")
|
195
141
|
import inspect
|
196
142
|
sig = inspect.signature(func)
|
143
|
+
# Set progress callback on tool instance if possible
|
144
|
+
instance = None
|
145
|
+
if hasattr(func, '__self__') and isinstance(func.__self__, ToolBase):
|
146
|
+
instance = func.__self__
|
147
|
+
instance._progress_callback = on_progress
|
197
148
|
if on_progress:
|
198
149
|
on_progress({
|
199
150
|
'event': 'start',
|
@@ -201,8 +152,6 @@ class ToolHandler:
|
|
201
152
|
'tool': tool_call.function.name,
|
202
153
|
'args': args
|
203
154
|
})
|
204
|
-
if 'on_progress' in sig.parameters and on_progress is not None:
|
205
|
-
args['on_progress'] = on_progress
|
206
155
|
try:
|
207
156
|
result = func(**args)
|
208
157
|
except Exception as e:
|
@@ -226,4 +175,22 @@ class ToolHandler:
|
|
226
175
|
'args': args,
|
227
176
|
'result': result
|
228
177
|
})
|
178
|
+
# Clean up progress callback
|
179
|
+
if instance is not None:
|
180
|
+
instance._progress_callback = None
|
229
181
|
return result
|
182
|
+
|
183
|
+
def _pytype_to_json_type(pytype):
|
184
|
+
import typing
|
185
|
+
if pytype == int:
|
186
|
+
return "integer"
|
187
|
+
elif pytype == float:
|
188
|
+
return "number"
|
189
|
+
elif pytype == bool:
|
190
|
+
return "boolean"
|
191
|
+
elif pytype == dict:
|
192
|
+
return "object"
|
193
|
+
elif pytype == list or pytype == typing.List:
|
194
|
+
return "array"
|
195
|
+
else:
|
196
|
+
return "string"
|
janito/agent/tools/__init__.py
CHANGED
@@ -1,12 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
from .get_lines import get_lines
|
4
|
-
from .replace_text_in_file import replace_text_in_file
|
5
|
-
from .find_files import find_files
|
6
|
-
from .run_bash_command import run_bash_command
|
7
|
-
from .fetch_url import fetch_url
|
8
|
-
from .python_exec import python_exec
|
9
|
-
from .py_compile import py_compile_file
|
10
|
-
from .search_files import search_files
|
11
|
-
from .remove_directory import remove_directory
|
1
|
+
import importlib
|
2
|
+
import os
|
12
3
|
|
4
|
+
# Dynamically import all tool modules in this directory (except __init__.py and tool_base.py)
|
5
|
+
_tool_dir = os.path.dirname(__file__)
|
6
|
+
for fname in os.listdir(_tool_dir):
|
7
|
+
if fname.endswith('.py') and fname not in ('__init__.py', 'tool_base.py'):
|
8
|
+
modname = fname[:-3]
|
9
|
+
importlib.import_module(f'janito.agent.tools.{modname}')
|
janito/agent/tools/ask_user.py
CHANGED
@@ -1,64 +1,61 @@
|
|
1
|
-
from janito.agent.
|
2
|
-
from
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
1
|
+
from janito.agent.tools.tool_base import ToolBase
|
2
|
+
from janito.agent.tools.rich_utils import print_info, print_success
|
3
|
+
|
4
|
+
class AskUserTool(ToolBase):
|
5
|
+
"""Ask the user a question and return their response."""
|
6
|
+
def call(self, question: str) -> str:
|
7
|
+
from rich import print as rich_print
|
8
|
+
from rich.panel import Panel
|
9
|
+
from prompt_toolkit import PromptSession
|
10
|
+
from prompt_toolkit.key_binding import KeyBindings
|
11
|
+
from prompt_toolkit.enums import EditingMode
|
12
|
+
from prompt_toolkit.formatted_text import HTML
|
13
|
+
from prompt_toolkit.styles import Style
|
14
|
+
|
15
|
+
rich_print(Panel.fit(question, title="Question", style="cyan"))
|
16
|
+
|
17
|
+
bindings = KeyBindings()
|
18
|
+
mode = {'multiline': False}
|
19
|
+
|
20
|
+
@bindings.add('c-r')
|
21
|
+
def _(event):
|
22
|
+
pass
|
23
|
+
|
24
|
+
style = Style.from_dict({
|
25
|
+
'bottom-toolbar': 'bg:#333333 #ffffff',
|
26
|
+
'b': 'bold',
|
27
|
+
'prompt': 'bold bg:#000080 #ffffff',
|
28
|
+
})
|
29
|
+
|
30
|
+
def get_toolbar():
|
31
|
+
if mode['multiline']:
|
32
|
+
return HTML('<b>Multiline mode (Esc+Enter to submit). Type /single to switch.</b>')
|
33
|
+
else:
|
34
|
+
return HTML('<b>Single-line mode (Enter to submit). Type /multi for multiline.</b>')
|
35
|
+
|
36
|
+
session = PromptSession(
|
37
|
+
multiline=False,
|
38
|
+
key_bindings=bindings,
|
39
|
+
editing_mode=EditingMode.EMACS,
|
40
|
+
bottom_toolbar=get_toolbar,
|
41
|
+
style=style
|
42
|
+
)
|
43
|
+
|
44
|
+
prompt_icon = HTML('<prompt>💬 </prompt>')
|
45
|
+
|
46
|
+
while True:
|
47
|
+
response = session.prompt(prompt_icon)
|
48
|
+
if not mode['multiline'] and response.strip() == '/multi':
|
49
|
+
mode['multiline'] = True
|
50
|
+
session.multiline = True
|
51
|
+
continue
|
52
|
+
elif mode['multiline'] and response.strip() == '/single':
|
53
|
+
mode['multiline'] = False
|
54
|
+
session.multiline = False
|
55
|
+
continue
|
56
|
+
else:
|
57
|
+
return response
|
23
58
|
|
24
|
-
mode = {'multiline': False}
|
25
59
|
|
26
|
-
|
27
|
-
|
28
|
-
# Disable reverse search
|
29
|
-
pass
|
30
|
-
|
31
|
-
style = Style.from_dict({
|
32
|
-
'bottom-toolbar': 'bg:#333333 #ffffff',
|
33
|
-
'b': 'bold',
|
34
|
-
'prompt': 'bold bg:#000080 #ffffff',
|
35
|
-
})
|
36
|
-
|
37
|
-
def get_toolbar():
|
38
|
-
if mode['multiline']:
|
39
|
-
return HTML('<b>Multiline mode (Esc+Enter to submit). Type /single to switch.</b>')
|
40
|
-
else:
|
41
|
-
return HTML('<b>Single-line mode (Enter to submit). Type /multi for multiline.</b>')
|
42
|
-
|
43
|
-
session = PromptSession(
|
44
|
-
multiline=False,
|
45
|
-
key_bindings=bindings,
|
46
|
-
editing_mode=EditingMode.EMACS,
|
47
|
-
bottom_toolbar=get_toolbar,
|
48
|
-
style=style
|
49
|
-
)
|
50
|
-
|
51
|
-
prompt_icon = HTML('<prompt>💬 </prompt>')
|
52
|
-
|
53
|
-
while True:
|
54
|
-
response = session.prompt(prompt_icon)
|
55
|
-
if not mode['multiline'] and response.strip() == '/multi':
|
56
|
-
mode['multiline'] = True
|
57
|
-
session.multiline = True
|
58
|
-
continue
|
59
|
-
elif mode['multiline'] and response.strip() == '/single':
|
60
|
-
mode['multiline'] = False
|
61
|
-
session.multiline = False
|
62
|
-
continue
|
63
|
-
else:
|
64
|
-
return response
|
60
|
+
from janito.agent.tool_handler import ToolHandler
|
61
|
+
ToolHandler.register_tool(AskUserTool, name="ask_user")
|
janito/agent/tools/fetch_url.py
CHANGED
@@ -1,40 +1,35 @@
|
|
1
1
|
import requests
|
2
|
-
from typing import Optional
|
2
|
+
from typing import Optional
|
3
3
|
from bs4 import BeautifulSoup
|
4
4
|
from janito.agent.tool_handler import ToolHandler
|
5
5
|
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
6
|
+
from janito.agent.tools.tool_base import ToolBase
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
class FetchUrlTool(ToolBase):
|
9
|
+
"""Fetch the content of a web page and extract its text."""
|
10
|
+
def call(self, url: str, search_strings: list[str] = None) -> str:
|
11
|
+
print_info(f"🌐 Fetching URL: {url} ... ")
|
12
|
+
response = requests.get(url, timeout=10)
|
13
|
+
response.raise_for_status()
|
14
|
+
self.update_progress(f"Fetched URL with status {response.status_code}")
|
15
|
+
soup = BeautifulSoup(response.text, 'html.parser')
|
16
|
+
text = soup.get_text(separator='\n')
|
11
17
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
if search_strings:
|
19
|
+
filtered = []
|
20
|
+
for s in search_strings:
|
21
|
+
idx = text.find(s)
|
22
|
+
if idx != -1:
|
23
|
+
start = max(0, idx - 200)
|
24
|
+
end = min(len(text), idx + len(s) + 200)
|
25
|
+
snippet = text[start:end]
|
26
|
+
filtered.append(snippet)
|
27
|
+
if filtered:
|
28
|
+
text = '\n...\n'.join(filtered)
|
29
|
+
else:
|
30
|
+
text = "No matches found for the provided search strings."
|
24
31
|
|
25
|
-
|
26
|
-
|
27
|
-
for s in search_strings:
|
28
|
-
idx = text.find(s)
|
29
|
-
if idx != -1:
|
30
|
-
start = max(0, idx - 200)
|
31
|
-
end = min(len(text), idx + len(s) + 200)
|
32
|
-
snippet = text[start:end]
|
33
|
-
filtered.append(snippet)
|
34
|
-
if filtered:
|
35
|
-
text = '\n...\n'.join(filtered)
|
36
|
-
else:
|
37
|
-
text = "No matches found for the provided search strings."
|
32
|
+
print_success("\u2705 Success")
|
33
|
+
return text
|
38
34
|
|
39
|
-
|
40
|
-
return text
|
35
|
+
ToolHandler.register_tool(FetchUrlTool, name="fetch_url")
|
janito/agent/tools/file_ops.py
CHANGED
@@ -1,72 +1,114 @@
|
|
1
1
|
import os
|
2
2
|
import shutil
|
3
3
|
from janito.agent.tool_handler import ToolHandler
|
4
|
-
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
4
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
5
|
+
from janito.agent.tools.utils import expand_path, display_path
|
6
|
+
from janito.agent.tools.tool_base import ToolBase
|
5
7
|
|
6
|
-
|
7
|
-
def create_file(path: str, content: str, overwrite: bool = False) -> str:
|
8
|
+
class CreateFileTool(ToolBase):
|
8
9
|
"""
|
9
10
|
Create a new file or update an existing file with the given content.
|
10
|
-
|
11
|
-
Args:
|
12
|
-
path (str): Path to the file to create or update.
|
13
|
-
content (str): Content to write to the file.
|
14
|
-
overwrite (bool): Whether to overwrite the file if it exists.
|
15
11
|
"""
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
if
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
12
|
+
def call(self, path: str, content: str, overwrite: bool = False) -> str:
|
13
|
+
original_path = path
|
14
|
+
path = expand_path(path)
|
15
|
+
updating = os.path.exists(path) and not os.path.isdir(path)
|
16
|
+
disp_path = display_path(original_path, path)
|
17
|
+
if os.path.exists(path):
|
18
|
+
if os.path.isdir(path):
|
19
|
+
print_error(f"❌ Error: is a directory")
|
20
|
+
return f"❌ Cannot create file: '{disp_path}' is an existing directory."
|
21
|
+
if not overwrite:
|
22
|
+
print_error(f"❗ Error: file '{disp_path}' exists and overwrite is False")
|
23
|
+
return f"❗ Cannot create file: '{disp_path}' already exists and overwrite is False."
|
24
|
+
if updating and overwrite:
|
25
|
+
print_info(f"📝 Updating file: '{disp_path}' ... ")
|
26
|
+
else:
|
27
|
+
print_info(f"📝 Creating file: '{disp_path}' ... ")
|
28
|
+
old_lines = None
|
29
|
+
if updating and overwrite:
|
30
|
+
with open(path, "r", encoding="utf-8") as f:
|
31
|
+
old_lines = sum(1 for _ in f)
|
32
|
+
with open(path, "w", encoding="utf-8") as f:
|
33
|
+
f.write(content)
|
34
|
+
print_success("✅ Success")
|
35
|
+
if old_lines is not None:
|
36
|
+
new_lines = content.count('\n') + 1 if content else 0
|
37
|
+
return f"✅ Successfully updated the file at '{disp_path}' ({old_lines} > {new_lines} lines)."
|
36
38
|
new_lines = content.count('\n') + 1 if content else 0
|
37
|
-
return f"✅ Successfully
|
38
|
-
else:
|
39
|
-
return f"✅ Successfully created the file at '{path}'."
|
39
|
+
return f"✅ Successfully created the file at '{disp_path}' ({new_lines} lines)."
|
40
40
|
|
41
|
+
class CreateDirectoryTool(ToolBase):
|
42
|
+
"""
|
43
|
+
Create a new directory at the specified path.
|
44
|
+
"""
|
45
|
+
def call(self, path: str, overwrite: bool = False) -> str:
|
46
|
+
"""
|
47
|
+
Create a new directory at the specified path.
|
48
|
+
Args:
|
49
|
+
path (str): Path to the directory to create.
|
50
|
+
overwrite (bool): Whether to remove the directory if it exists.
|
51
|
+
Returns:
|
52
|
+
str: Result message.
|
53
|
+
"""
|
54
|
+
original_path = path
|
55
|
+
path = expand_path(path)
|
56
|
+
disp_path = display_path(original_path, path)
|
57
|
+
if os.path.exists(path):
|
58
|
+
if not os.path.isdir(path):
|
59
|
+
print_error(f"❌ Path '{disp_path}' exists and is not a directory.")
|
60
|
+
return f"❌ Path '{disp_path}' exists and is not a directory."
|
61
|
+
if not overwrite:
|
62
|
+
print_error(f"❗ Directory '{disp_path}' already exists and overwrite is False.")
|
63
|
+
return f"❗ Directory '{disp_path}' already exists and overwrite is False."
|
64
|
+
# Remove existing directory if overwrite is True
|
65
|
+
shutil.rmtree(path)
|
66
|
+
print_info(f"🗑️ Removed existing directory: '{disp_path}'")
|
67
|
+
os.makedirs(path, exist_ok=True)
|
68
|
+
print_success(f"✅ Created directory: '{disp_path}'")
|
69
|
+
return f"✅ Successfully created directory at '{disp_path}'."
|
41
70
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
71
|
+
class RemoveFileTool(ToolBase):
|
72
|
+
"""
|
73
|
+
Remove a file at the specified path.
|
74
|
+
"""
|
75
|
+
def call(self, path: str) -> str:
|
76
|
+
original_path = path
|
77
|
+
path = expand_path(path)
|
78
|
+
disp_path = display_path(original_path, path)
|
79
|
+
print_info(f"🗑️ Removing file: '{disp_path}' ... ")
|
80
|
+
os.remove(path)
|
81
|
+
print_success("✅ Success")
|
82
|
+
return f"✅ Successfully deleted the file at '{disp_path}'."
|
48
83
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
84
|
+
class MoveFileTool(ToolBase):
|
85
|
+
"""
|
86
|
+
Move or rename a file from source to destination.
|
87
|
+
"""
|
88
|
+
def call(self, source_path: str, destination_path: str, overwrite: bool = False) -> str:
|
89
|
+
orig_source = source_path
|
90
|
+
orig_dest = destination_path
|
91
|
+
source_path = expand_path(source_path)
|
92
|
+
destination_path = expand_path(destination_path)
|
93
|
+
disp_source = display_path(orig_source, source_path)
|
94
|
+
disp_dest = display_path(orig_dest, destination_path)
|
95
|
+
print_info(f"🚚 Moving '{disp_source}' to '{disp_dest}' ... ")
|
96
|
+
if not os.path.exists(source_path):
|
97
|
+
print_error(f"❌ Error: source does not exist")
|
98
|
+
return f"❌ Source path '{disp_source}' does not exist."
|
99
|
+
if os.path.exists(destination_path):
|
100
|
+
if not overwrite:
|
101
|
+
print_error(f"❗ Error: destination exists and overwrite is False")
|
102
|
+
return f"❗ Destination path '{disp_dest}' already exists and overwrite is False."
|
103
|
+
if os.path.isdir(destination_path):
|
104
|
+
print_error(f"❌ Error: destination is a directory")
|
105
|
+
return f"❌ Destination path '{disp_dest}' is an existing directory."
|
106
|
+
shutil.move(source_path, destination_path)
|
107
|
+
print_success("✅ Success")
|
108
|
+
return f"✅ Successfully moved '{disp_source}' to '{disp_dest}'."
|
66
109
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
return f"✅ Directory '{path}' created successfully."
|
110
|
+
# register tools
|
111
|
+
ToolHandler.register_tool(CreateFileTool, name="create_file")
|
112
|
+
ToolHandler.register_tool(CreateDirectoryTool, name="create_directory")
|
113
|
+
ToolHandler.register_tool(RemoveFileTool, name="remove_file")
|
114
|
+
ToolHandler.register_tool(MoveFileTool, name="move_file")
|