janito 1.8.0__py3-none-any.whl → 1.9.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/config_defaults.py +23 -0
- janito/agent/config_utils.py +0 -9
- janito/agent/conversation.py +31 -9
- janito/agent/conversation_api.py +32 -2
- janito/agent/conversation_history.py +53 -0
- janito/agent/conversation_tool_calls.py +11 -8
- janito/agent/openai_client.py +11 -3
- janito/agent/openai_schema_generator.py +9 -6
- janito/agent/providers.py +77 -0
- janito/agent/rich_message_handler.py +1 -1
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +8 -8
- janito/agent/tool_executor.py +18 -10
- janito/agent/tool_use_tracker.py +16 -0
- janito/agent/tools/__init__.py +7 -9
- janito/agent/tools/create_directory.py +7 -6
- janito/agent/tools/create_file.py +29 -54
- janito/agent/tools/delete_text_in_file.py +97 -0
- janito/agent/tools/fetch_url.py +11 -3
- janito/agent/tools/find_files.py +37 -25
- janito/agent/tools/get_file_outline/__init__.py +1 -0
- janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +12 -15
- janito/agent/tools/get_file_outline/python_outline.py +134 -0
- janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +9 -0
- janito/agent/tools/get_lines.py +15 -11
- janito/agent/tools/move_file.py +10 -11
- janito/agent/tools/remove_directory.py +2 -2
- janito/agent/tools/remove_file.py +11 -13
- janito/agent/tools/replace_file.py +62 -0
- janito/agent/tools/replace_text_in_file.py +3 -3
- janito/agent/tools/run_bash_command.py +3 -7
- janito/agent/tools/run_powershell_command.py +39 -28
- janito/agent/tools/run_python_command.py +3 -5
- janito/agent/tools/search_text.py +10 -14
- janito/agent/tools/validate_file_syntax/__init__.py +1 -0
- janito/agent/tools/validate_file_syntax/core.py +92 -0
- janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
- janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
- janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
- janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
- janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
- janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
- janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
- janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
- janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
- janito/agent/tools_utils/__init__.py +1 -0
- janito/agent/tools_utils/dir_walk_utils.py +23 -0
- janito/agent/{tools/outline_file → tools_utils}/formatting.py +5 -2
- janito/agent/{tools → tools_utils}/gitignore_utils.py +0 -3
- janito/agent/tools_utils/utils.py +30 -0
- janito/cli/_livereload_log_utils.py +13 -0
- janito/cli/arg_parser.py +45 -3
- janito/cli/{runner/cli_main.py → cli_main.py} +120 -20
- janito/cli/livereload_starter.py +60 -0
- janito/cli/main.py +110 -21
- janito/cli/one_shot.py +66 -0
- janito/cli/termweb_starter.py +2 -2
- janito/livereload/app.py +25 -0
- janito/rich_utils.py +0 -22
- janito/{cli_chat_shell → shell}/commands/__init__.py +18 -11
- janito/{cli_chat_shell → shell}/commands/config.py +4 -4
- janito/shell/commands/conversation_restart.py +72 -0
- janito/shell/commands/edit.py +21 -0
- janito/shell/commands/history_view.py +18 -0
- janito/shell/commands/livelogs.py +40 -0
- janito/{cli_chat_shell → shell}/commands/prompt.py +10 -6
- janito/shell/commands/session.py +32 -0
- janito/{cli_chat_shell → shell}/commands/session_control.py +2 -7
- janito/{cli_chat_shell → shell}/commands/sum.py +6 -6
- janito/{cli_chat_shell → shell}/commands/termweb_log.py +10 -10
- janito/shell/commands/tools.py +23 -0
- janito/{cli_chat_shell → shell}/commands/utility.py +5 -4
- janito/{cli_chat_shell → shell}/commands/verbose.py +1 -1
- janito/shell/commands.py +40 -0
- janito/shell/main.py +321 -0
- janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
- janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
- janito/{cli_chat_shell/session_manager.py → shell/session/manager.py} +53 -3
- janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -15
- janito/termweb/app.py +3 -3
- janito/termweb/static/editor.css +146 -0
- janito/termweb/static/editor.css.bak +27 -0
- janito/termweb/static/editor.html +15 -213
- janito/termweb/static/editor.html.bak +16 -215
- janito/termweb/static/editor.js +209 -0
- janito/termweb/static/editor.js.bak +227 -0
- janito/termweb/static/index.html +2 -3
- janito/termweb/static/index.html.bak +2 -3
- janito/termweb/static/termweb.css.bak +33 -84
- janito/termweb/static/termweb.js +15 -34
- janito/termweb/static/termweb.js.bak +18 -36
- {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/METADATA +6 -3
- janito-1.9.0.dist-info/RECORD +151 -0
- {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/WHEEL +1 -1
- janito/agent/tools/dir_walk_utils.py +0 -16
- janito/agent/tools/memory.py +0 -48
- janito/agent/tools/outline_file/python_outline.py +0 -71
- janito/agent/tools/present_choices_test.py +0 -18
- janito/agent/tools/rich_live.py +0 -44
- janito/agent/tools/tools_utils.py +0 -56
- janito/agent/tools/utils.py +0 -33
- janito/agent/tools/validate_file_syntax.py +0 -163
- janito/cli_chat_shell/chat_loop.py +0 -163
- janito/cli_chat_shell/chat_state.py +0 -38
- janito/cli_chat_shell/commands/history_start.py +0 -37
- janito/cli_chat_shell/commands/session.py +0 -48
- janito-1.8.0.dist-info/RECORD +0 -127
- /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
- /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
- /janito/cli/{runner/config.py → config_runner.py} +0 -0
- /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
- /janito/{cli/runner → shell}/__init__.py +0 -0
- /janito/{cli_chat_shell → shell}/commands/lang.py +0 -0
- /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
- /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
- /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
- {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/entry_points.txt +0 -0
- {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/top_level.txt +0 -0
@@ -1,78 +1,53 @@
|
|
1
1
|
import os
|
2
|
-
import shutil
|
3
2
|
from janito.agent.tool_registry import register_tool
|
4
|
-
|
3
|
+
|
4
|
+
# from janito.agent.tools_utils.expand_path import expand_path
|
5
|
+
from janito.agent.tools_utils.utils import display_path
|
5
6
|
from janito.agent.tool_base import ToolBase
|
6
7
|
from janito.i18n import tr
|
7
8
|
|
8
9
|
|
10
|
+
from janito.agent.tools.validate_file_syntax.core import validate_file_syntax
|
11
|
+
|
12
|
+
|
9
13
|
@register_tool(name="create_file")
|
10
14
|
class CreateFileTool(ToolBase):
|
11
15
|
"""
|
12
|
-
Create a new file with the given content
|
16
|
+
Create a new file with the given content.
|
13
17
|
Args:
|
14
|
-
file_path (str): Path to the file to create
|
18
|
+
file_path (str): Path to the file to create.
|
15
19
|
content (str): Content to write to the file.
|
16
|
-
overwrite (bool, optional): If True, overwrite the file if it exists. Defaults to False.
|
17
|
-
CRITICAL: If you use overwrite=True, you MUST provide the full content for the file. Using placeholders or partial content will result in file corruption. Before overwriting, read the full original file.
|
18
20
|
Returns:
|
19
21
|
str: Status message indicating the result. Example:
|
20
22
|
- "✅ Successfully created the file at ..."
|
21
23
|
"""
|
22
24
|
|
23
|
-
def run(self, file_path: str, content: str
|
24
|
-
expanded_file_path =
|
25
|
+
def run(self, file_path: str, content: str) -> str:
|
26
|
+
expanded_file_path = file_path # Using file_path as is
|
25
27
|
disp_path = display_path(expanded_file_path)
|
26
28
|
file_path = expanded_file_path
|
27
|
-
backup_path = None
|
28
29
|
if os.path.exists(file_path):
|
29
|
-
if not overwrite:
|
30
|
-
return tr(
|
31
|
-
"⚠️ File already exists at '{disp_path}'. Use overwrite=True to overwrite.",
|
32
|
-
disp_path=disp_path,
|
33
|
-
)
|
34
|
-
# Check ToolUseTracker for full read before overwrite
|
35
30
|
try:
|
36
|
-
|
37
|
-
|
38
|
-
tracker = ToolUseTracker()
|
39
|
-
if not tracker.file_fully_read(file_path):
|
40
|
-
self.report_error(
|
41
|
-
"❌ Refusing to overwrite file: full file has not been read with get_lines."
|
42
|
-
)
|
43
|
-
return tr(
|
44
|
-
"❌ Refusing to overwrite file: full file has not been read with get_lines."
|
45
|
-
)
|
31
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
32
|
+
existing_content = f.read()
|
46
33
|
except Exception as e:
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
tr("📝 Updating file: '{disp_path}' ...", disp_path=disp_path)
|
53
|
-
)
|
54
|
-
mode = "w"
|
55
|
-
updated = True
|
56
|
-
else:
|
57
|
-
dir_name = os.path.dirname(file_path)
|
58
|
-
if dir_name:
|
59
|
-
os.makedirs(dir_name, exist_ok=True)
|
60
|
-
self.report_info(
|
61
|
-
tr("📝 Creating file: '{disp_path}' ...", disp_path=disp_path)
|
34
|
+
existing_content = f"[Error reading file: {e}]"
|
35
|
+
return tr(
|
36
|
+
"❗ Cannot create file: file already exists at '{disp_path}'.\n--- Current file content ---\n{existing_content}",
|
37
|
+
disp_path=disp_path,
|
38
|
+
existing_content=existing_content,
|
62
39
|
)
|
63
|
-
|
64
|
-
|
65
|
-
|
40
|
+
dir_name = os.path.dirname(file_path)
|
41
|
+
if dir_name:
|
42
|
+
os.makedirs(dir_name, exist_ok=True)
|
43
|
+
self.report_info(tr("📝 Creating file '{disp_path}' ...", disp_path=disp_path))
|
44
|
+
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
66
45
|
f.write(content)
|
67
46
|
new_lines = content.count("\n") + 1 if content else 0
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
return msg
|
76
|
-
else:
|
77
|
-
self.report_success(tr("✅ ({new_lines} lines).", new_lines=new_lines))
|
78
|
-
return tr("✅ Created file ({new_lines} lines).", new_lines=new_lines)
|
47
|
+
self.report_success(tr("✅ {new_lines} lines", new_lines=new_lines))
|
48
|
+
# Perform syntax validation and append result
|
49
|
+
validation_result = validate_file_syntax(file_path)
|
50
|
+
return (
|
51
|
+
tr("✅ Created file {new_lines} lines.", new_lines=new_lines)
|
52
|
+
+ f"\n{validation_result}"
|
53
|
+
)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_registry import register_tool
|
3
|
+
from janito.i18n import tr
|
4
|
+
|
5
|
+
|
6
|
+
@register_tool(name="delete_text_in_file")
|
7
|
+
class DeleteTextInFileTool(ToolBase):
|
8
|
+
"""
|
9
|
+
Delete all occurrences of text between start_marker and end_marker (inclusive) in a file, using exact string markers.
|
10
|
+
|
11
|
+
Args:
|
12
|
+
file_path (str): Path to the file to modify.
|
13
|
+
start_marker (str): The starting delimiter string.
|
14
|
+
end_marker (str): The ending delimiter string.
|
15
|
+
backup (bool, optional): If True, create a backup (.bak) before deleting. Defaults to False.
|
16
|
+
Returns:
|
17
|
+
str: Status message indicating the result.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def run(
|
21
|
+
self,
|
22
|
+
file_path: str,
|
23
|
+
start_marker: str,
|
24
|
+
end_marker: str,
|
25
|
+
backup: bool = False,
|
26
|
+
) -> str:
|
27
|
+
import shutil
|
28
|
+
from janito.agent.tools_utils.utils import display_path
|
29
|
+
|
30
|
+
disp_path = display_path(file_path)
|
31
|
+
backup_path = file_path + ".bak"
|
32
|
+
backup_msg = ""
|
33
|
+
try:
|
34
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
35
|
+
content = f.read()
|
36
|
+
except Exception as e:
|
37
|
+
self.report_error(tr(" ❌ Error reading file: {error}", error=e))
|
38
|
+
return tr("Error reading file: {error}", error=e)
|
39
|
+
|
40
|
+
start_count = content.count(start_marker)
|
41
|
+
if start_count > 1:
|
42
|
+
self.report_error("Need more context for start_marker")
|
43
|
+
return (
|
44
|
+
f"Error: start_marker is not unique in {disp_path}. "
|
45
|
+
"Try including the next line(s) for more context."
|
46
|
+
)
|
47
|
+
|
48
|
+
end_count = content.count(end_marker)
|
49
|
+
if end_count > 1:
|
50
|
+
self.report_error("Need more context for end_marker")
|
51
|
+
return (
|
52
|
+
f"Error: end_marker is not unique in {disp_path}. "
|
53
|
+
"Try including the previous line(s) for more context."
|
54
|
+
)
|
55
|
+
|
56
|
+
count = 0
|
57
|
+
new_content = content
|
58
|
+
while True:
|
59
|
+
start_idx = new_content.find(start_marker)
|
60
|
+
if start_idx == -1:
|
61
|
+
break
|
62
|
+
end_idx = new_content.find(end_marker, start_idx + len(start_marker))
|
63
|
+
if end_idx == -1:
|
64
|
+
break
|
65
|
+
# Remove from start_marker to end_marker (inclusive)
|
66
|
+
new_content = (
|
67
|
+
new_content[:start_idx] + new_content[end_idx + len(end_marker) :]
|
68
|
+
)
|
69
|
+
count += 1
|
70
|
+
|
71
|
+
if count == 0:
|
72
|
+
self.report_warning(tr("ℹ️ No blocks found between markers."))
|
73
|
+
return tr(
|
74
|
+
"No blocks found between markers in {file_path}.", file_path=file_path
|
75
|
+
)
|
76
|
+
|
77
|
+
if backup:
|
78
|
+
shutil.copy2(file_path, backup_path)
|
79
|
+
backup_msg = f" (A backup was saved to {backup_path})"
|
80
|
+
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
81
|
+
f.write(new_content)
|
82
|
+
|
83
|
+
self.report_success(
|
84
|
+
tr(
|
85
|
+
"Deleted {count} block(s) between markers in {disp_path}.",
|
86
|
+
count=count,
|
87
|
+
disp_path=disp_path,
|
88
|
+
)
|
89
|
+
)
|
90
|
+
return (
|
91
|
+
tr(
|
92
|
+
"Deleted {count} block(s) between markers in {file_path}.",
|
93
|
+
count=count,
|
94
|
+
file_path=file_path,
|
95
|
+
)
|
96
|
+
+ backup_msg
|
97
|
+
)
|
janito/agent/tools/fetch_url.py
CHANGED
@@ -3,6 +3,7 @@ from bs4 import BeautifulSoup
|
|
3
3
|
from janito.agent.tool_registry import register_tool
|
4
4
|
from janito.agent.tool_base import ToolBase
|
5
5
|
from janito.i18n import tr
|
6
|
+
from janito.agent.tools_utils.utils import pluralize
|
6
7
|
|
7
8
|
|
8
9
|
@register_tool(name="fetch_url")
|
@@ -21,9 +22,9 @@ class FetchUrlTool(ToolBase):
|
|
21
22
|
|
22
23
|
def run(self, url: str, search_strings: list[str] = None) -> str:
|
23
24
|
if not url.strip():
|
24
|
-
self.report_warning(tr("
|
25
|
+
self.report_warning(tr("ℹ️ Empty URL provided."))
|
25
26
|
return tr("Warning: Empty URL provided. Operation skipped.")
|
26
|
-
self.report_info(tr("🌐 Fetching URL
|
27
|
+
self.report_info(tr("🌐 Fetching URL '{url}' ...", url=url))
|
27
28
|
response = requests.get(url, timeout=10)
|
28
29
|
response.raise_for_status()
|
29
30
|
self.update_progress(
|
@@ -49,5 +50,12 @@ class FetchUrlTool(ToolBase):
|
|
49
50
|
text = "\n...\n".join(filtered)
|
50
51
|
else:
|
51
52
|
text = tr("No lines found for the provided search strings.")
|
52
|
-
|
53
|
+
num_lines = len(text.splitlines())
|
54
|
+
self.report_success(
|
55
|
+
tr(
|
56
|
+
"✅ {num_lines} {line_word}",
|
57
|
+
num_lines=num_lines,
|
58
|
+
line_word=pluralize("line", num_lines),
|
59
|
+
)
|
60
|
+
)
|
53
61
|
return text
|
janito/agent/tools/find_files.py
CHANGED
@@ -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.
|
4
|
-
from janito.agent.
|
3
|
+
from janito.agent.tools_utils.utils import pluralize, display_path
|
4
|
+
from janito.agent.tools_utils.dir_walk_utils import walk_dir_with_gitignore
|
5
5
|
from janito.i18n import tr
|
6
6
|
import fnmatch
|
7
7
|
import os
|
@@ -10,11 +10,12 @@ import os
|
|
10
10
|
@register_tool(name="find_files")
|
11
11
|
class FindFilesTool(ToolBase):
|
12
12
|
"""
|
13
|
-
Find files in one or more directories matching a pattern. Respects .gitignore.
|
13
|
+
Find files or directories in one or more directories matching a pattern. Respects .gitignore.
|
14
14
|
Args:
|
15
15
|
paths (str): String of one or more paths (space-separated) to search in. Each path can be a directory.
|
16
16
|
pattern (str): File pattern(s) to match. Multiple patterns can be separated by spaces. Uses Unix shell-style wildcards (fnmatch), e.g. '*.py', 'data_??.csv', '[a-z]*.txt'.
|
17
|
-
|
17
|
+
- If the pattern ends with '/' or '\\', only matching directory names (with trailing slash) are returned, not the files within those directories. For example, pattern '*/' will return only directories at the specified depth.
|
18
|
+
max_depth (int, optional): Maximum directory depth to search. If None, unlimited recursion. If 0, only the top-level directory. If 1, only the root directory (matches 'find . -maxdepth 1').
|
18
19
|
max_results (int, optional): Maximum number of results to return. 0 means no limit (default).
|
19
20
|
Returns:
|
20
21
|
str: Newline-separated list of matching file paths. Example:
|
@@ -23,19 +24,17 @@ class FindFilesTool(ToolBase):
|
|
23
24
|
If max_results is reached, appends a note to the output.
|
24
25
|
"""
|
25
26
|
|
26
|
-
def run(self, paths: str, pattern: str, max_depth: int =
|
27
|
+
def run(self, paths: str, pattern: str, max_depth: int = None) -> str:
|
27
28
|
if not pattern:
|
28
|
-
self.report_warning(
|
29
|
-
tr("⚠️ Warning: Empty file pattern provided. Operation skipped.")
|
30
|
-
)
|
29
|
+
self.report_warning(tr("ℹ️ Empty file pattern provided."))
|
31
30
|
return tr("Warning: Empty file pattern provided. Operation skipped.")
|
32
|
-
output = set()
|
33
31
|
patterns = pattern.split()
|
32
|
+
results = []
|
34
33
|
for directory in paths.split():
|
35
34
|
disp_path = display_path(directory)
|
36
35
|
depth_msg = (
|
37
36
|
tr(" (max depth: {max_depth})", max_depth=max_depth)
|
38
|
-
if max_depth > 0
|
37
|
+
if max_depth is not None and max_depth > 0
|
39
38
|
else ""
|
40
39
|
)
|
41
40
|
self.report_info(
|
@@ -46,24 +45,37 @@ class FindFilesTool(ToolBase):
|
|
46
45
|
depth_msg=depth_msg,
|
47
46
|
)
|
48
47
|
)
|
48
|
+
dir_output = set()
|
49
49
|
for root, dirs, files in walk_dir_with_gitignore(
|
50
50
|
directory, max_depth=max_depth
|
51
51
|
):
|
52
52
|
for pat in patterns:
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
53
|
+
# Directory matching: pattern ends with '/' or '\'
|
54
|
+
if pat.endswith("/") or pat.endswith("\\"):
|
55
|
+
dir_pat = pat.rstrip("/\\")
|
56
|
+
for d in dirs:
|
57
|
+
if fnmatch.fnmatch(d, dir_pat):
|
58
|
+
dir_output.add(os.path.join(root, d) + os.sep)
|
59
|
+
else:
|
60
|
+
# Match files
|
61
|
+
for filename in fnmatch.filter(files, pat):
|
62
|
+
dir_output.add(os.path.join(root, filename))
|
63
|
+
# Also match directories (without trailing slash)
|
64
|
+
for d in fnmatch.filter(dirs, pat):
|
65
|
+
dir_output.add(os.path.join(root, d))
|
66
|
+
self.report_success(
|
67
|
+
tr(
|
68
|
+
" ✅ {count} {file_word}",
|
69
|
+
count=len(dir_output),
|
70
|
+
file_word=pluralize("file", len(dir_output)),
|
71
|
+
)
|
60
72
|
)
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
result = "\n".join(
|
73
|
+
# If searching in '.', strip leading './' from results
|
74
|
+
if directory.strip() == ".":
|
75
|
+
dir_output = {
|
76
|
+
p[2:] if (p.startswith("./") or p.startswith(".\\")) else p
|
77
|
+
for p in dir_output
|
78
|
+
}
|
79
|
+
results.extend(sorted(dir_output))
|
80
|
+
result = "\n".join(results)
|
69
81
|
return result
|
@@ -0,0 +1 @@
|
|
1
|
+
# Outline tools and parsers package
|
@@ -1,14 +1,17 @@
|
|
1
1
|
from janito.agent.tool_registry import register_tool
|
2
2
|
from .python_outline import parse_python_outline
|
3
3
|
from .markdown_outline import parse_markdown_outline
|
4
|
-
from .formatting import
|
4
|
+
from janito.agent.tools_utils.formatting import (
|
5
|
+
format_outline_table,
|
6
|
+
format_markdown_outline_table,
|
7
|
+
)
|
5
8
|
import os
|
6
9
|
from janito.agent.tool_base import ToolBase
|
7
|
-
from janito.agent.
|
10
|
+
from janito.agent.tools_utils.utils import display_path, pluralize
|
8
11
|
from janito.i18n import tr
|
9
12
|
|
10
13
|
|
11
|
-
@register_tool(name="
|
14
|
+
@register_tool(name="get_file_outline")
|
12
15
|
class GetFileOutlineTool(ToolBase):
|
13
16
|
"""
|
14
17
|
Get an outline of a file's structure. Supports Python and Markdown files.
|
@@ -21,7 +24,7 @@ class GetFileOutlineTool(ToolBase):
|
|
21
24
|
try:
|
22
25
|
self.report_info(
|
23
26
|
tr(
|
24
|
-
"📄 Outlining file
|
27
|
+
"📄 Outlining file '{disp_path}' ...",
|
25
28
|
disp_path=display_path(file_path),
|
26
29
|
)
|
27
30
|
)
|
@@ -34,9 +37,9 @@ class GetFileOutlineTool(ToolBase):
|
|
34
37
|
table = format_outline_table(outline_items)
|
35
38
|
self.report_success(
|
36
39
|
tr(
|
37
|
-
"✅ {count}
|
40
|
+
"✅ Outlined {count} {item_word}",
|
38
41
|
count=len(outline_items),
|
39
|
-
|
42
|
+
item_word=pluralize("item", len(outline_items)),
|
40
43
|
)
|
41
44
|
)
|
42
45
|
return (
|
@@ -53,9 +56,9 @@ class GetFileOutlineTool(ToolBase):
|
|
53
56
|
table = format_markdown_outline_table(outline_items)
|
54
57
|
self.report_success(
|
55
58
|
tr(
|
56
|
-
"✅ {count}
|
59
|
+
"✅ Outlined {count} {item_word}",
|
57
60
|
count=len(outline_items),
|
58
|
-
|
61
|
+
item_word=pluralize("item", len(outline_items)),
|
59
62
|
)
|
60
63
|
)
|
61
64
|
return (
|
@@ -68,13 +71,7 @@ class GetFileOutlineTool(ToolBase):
|
|
68
71
|
)
|
69
72
|
else:
|
70
73
|
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
|
-
)
|
74
|
+
self.report_success(tr("✅ Outlined {count} items", count=len(lines)))
|
78
75
|
return tr(
|
79
76
|
"Outline: {count} lines ({outline_type})\nFile has {count} lines.",
|
80
77
|
count=len(lines),
|
@@ -0,0 +1,134 @@
|
|
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
|
+
main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
|
10
|
+
outline = []
|
11
|
+
stack = [] # (type, name, indent, start, parent)
|
12
|
+
obj_ranges = [] # (type, name, start, end, parent, indent)
|
13
|
+
last_top_obj = None
|
14
|
+
for idx, line in enumerate(lines):
|
15
|
+
class_match = class_pat.match(line)
|
16
|
+
func_match = func_pat.match(line)
|
17
|
+
assign_match = assign_pat.match(line)
|
18
|
+
indent = len(line) - len(line.lstrip())
|
19
|
+
# If a new top-level class or function starts, close the previous one
|
20
|
+
if (class_match or func_match) and indent == 0 and last_top_obj:
|
21
|
+
# Only close if still open
|
22
|
+
if last_top_obj in stack:
|
23
|
+
stack.remove(last_top_obj)
|
24
|
+
obj_ranges.append(
|
25
|
+
(
|
26
|
+
last_top_obj[0],
|
27
|
+
last_top_obj[1],
|
28
|
+
last_top_obj[3],
|
29
|
+
idx,
|
30
|
+
last_top_obj[4],
|
31
|
+
last_top_obj[2],
|
32
|
+
)
|
33
|
+
)
|
34
|
+
last_top_obj = None
|
35
|
+
if class_match:
|
36
|
+
name = class_match.group(2)
|
37
|
+
parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
|
38
|
+
obj = ("class", name, indent, idx + 1, parent)
|
39
|
+
stack.append(obj)
|
40
|
+
if indent == 0:
|
41
|
+
last_top_obj = obj
|
42
|
+
elif func_match:
|
43
|
+
name = func_match.group(2)
|
44
|
+
parent = ""
|
45
|
+
for s in reversed(stack):
|
46
|
+
if s[0] == "class" and indent > s[2]:
|
47
|
+
parent = s[1]
|
48
|
+
break
|
49
|
+
obj = ("function", name, indent, idx + 1, parent)
|
50
|
+
stack.append(obj)
|
51
|
+
if indent == 0:
|
52
|
+
last_top_obj = obj
|
53
|
+
elif assign_match and indent == 0:
|
54
|
+
var_name = assign_match.group(2)
|
55
|
+
var_type = "const" if var_name.isupper() else "var"
|
56
|
+
outline.append(
|
57
|
+
{
|
58
|
+
"type": var_type,
|
59
|
+
"name": var_name,
|
60
|
+
"start": idx + 1,
|
61
|
+
"end": idx + 1,
|
62
|
+
"parent": "",
|
63
|
+
"docstring": "",
|
64
|
+
}
|
65
|
+
)
|
66
|
+
main_match = main_pat.match(line)
|
67
|
+
if main_match:
|
68
|
+
outline.append(
|
69
|
+
{
|
70
|
+
"type": "main",
|
71
|
+
"name": "__main__",
|
72
|
+
"start": idx + 1,
|
73
|
+
"end": idx + 1,
|
74
|
+
"parent": "",
|
75
|
+
"docstring": "",
|
76
|
+
}
|
77
|
+
)
|
78
|
+
while stack and indent < stack[-1][2]:
|
79
|
+
popped = stack.pop()
|
80
|
+
obj_ranges.append(
|
81
|
+
(popped[0], popped[1], popped[3], idx, popped[4], popped[2])
|
82
|
+
)
|
83
|
+
# Close any remaining open objects
|
84
|
+
for popped in stack:
|
85
|
+
obj_ranges.append(
|
86
|
+
(popped[0], popped[1], popped[3], len(lines), popped[4], popped[2])
|
87
|
+
)
|
88
|
+
|
89
|
+
# Now, extract docstrings for classes, functions, and methods
|
90
|
+
for obj in obj_ranges:
|
91
|
+
obj_type, name, start, end, parent, indent = obj
|
92
|
+
# Determine if this is a method
|
93
|
+
if obj_type == "function" and parent:
|
94
|
+
outline_type = "method"
|
95
|
+
elif obj_type == "function":
|
96
|
+
outline_type = "function"
|
97
|
+
else:
|
98
|
+
outline_type = obj_type
|
99
|
+
docstring = extract_docstring(lines, start, end)
|
100
|
+
outline.append(
|
101
|
+
{
|
102
|
+
"type": outline_type,
|
103
|
+
"name": name,
|
104
|
+
"start": start,
|
105
|
+
"end": end,
|
106
|
+
"parent": parent,
|
107
|
+
"docstring": docstring,
|
108
|
+
}
|
109
|
+
)
|
110
|
+
return outline
|
111
|
+
|
112
|
+
|
113
|
+
def extract_docstring(lines, start_idx, end_idx):
|
114
|
+
"""Extracts a docstring from lines[start_idx:end_idx] if present."""
|
115
|
+
for i in range(start_idx, min(end_idx, len(lines))):
|
116
|
+
line = lines[i].lstrip()
|
117
|
+
if not line:
|
118
|
+
continue
|
119
|
+
if line.startswith('"""') or line.startswith("'''"):
|
120
|
+
quote = line[:3]
|
121
|
+
doc = line[3:]
|
122
|
+
if doc.strip().endswith(quote):
|
123
|
+
return doc.strip()[:-3].strip()
|
124
|
+
docstring_lines = [doc]
|
125
|
+
for j in range(i + 1, min(end_idx, len(lines))):
|
126
|
+
line = lines[j]
|
127
|
+
if line.strip().endswith(quote):
|
128
|
+
docstring_lines.append(line.strip()[:-3])
|
129
|
+
return "\n".join([d.strip() for d in docstring_lines]).strip()
|
130
|
+
docstring_lines.append(line)
|
131
|
+
break
|
132
|
+
else:
|
133
|
+
break
|
134
|
+
return ""
|
@@ -9,6 +9,15 @@ class SearchOutlineTool(ToolBase):
|
|
9
9
|
"""
|
10
10
|
|
11
11
|
def run(self, file_path: str) -> str:
|
12
|
+
from janito.agent.tools_utils.utils import display_path
|
13
|
+
from janito.i18n import tr
|
14
|
+
|
15
|
+
self.report_info(
|
16
|
+
tr(
|
17
|
+
"🔍 Searching for outline in '{disp_path}'",
|
18
|
+
disp_path=display_path(file_path),
|
19
|
+
)
|
20
|
+
)
|
12
21
|
# ... rest of implementation ...
|
13
22
|
# Example warnings and successes:
|
14
23
|
# self.report_warning(tr("No files found with supported extensions."))
|
janito/agent/tools/get_lines.py
CHANGED
@@ -1,40 +1,44 @@
|
|
1
1
|
from janito.agent.tool_base import ToolBase
|
2
2
|
from janito.agent.tool_registry import register_tool
|
3
|
-
from janito.agent.
|
3
|
+
from janito.agent.tools_utils.utils import pluralize
|
4
4
|
from janito.i18n import tr
|
5
5
|
|
6
6
|
|
7
7
|
@register_tool(name="get_lines")
|
8
8
|
class GetLinesTool(ToolBase):
|
9
9
|
"""
|
10
|
-
Read lines from a file.
|
10
|
+
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.
|
11
|
+
|
11
12
|
Args:
|
12
13
|
file_path (str): Path to the file to read lines from.
|
13
|
-
from_line (int, optional): Starting line number (1-based).
|
14
|
-
to_line (int, optional): Ending line number (1-based).
|
14
|
+
from_line (int, optional): Starting line number (1-based). Omit to start from the first line.
|
15
|
+
to_line (int, optional): Ending line number (1-based). Omit to read to the end of the file.
|
16
|
+
|
17
|
+
To read the full file, just provide file_path and leave from_line and to_line unset.
|
18
|
+
|
15
19
|
Returns:
|
16
20
|
str: File content with a header indicating the file name and line range. Example:
|
17
21
|
- "---\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...>"
|
22
|
+
- "---\nFile: /path/to/file.py | All lines (total: 100 (all))\n---\n<all lines...>"
|
19
23
|
- "Error reading file: <error message>"
|
20
24
|
- "❗ not found"
|
21
25
|
"""
|
22
26
|
|
23
27
|
def run(self, file_path: str, from_line: int = None, to_line: int = None) -> str:
|
24
|
-
from janito.agent.
|
28
|
+
from janito.agent.tools_utils.utils import display_path
|
25
29
|
|
26
30
|
disp_path = display_path(file_path)
|
27
31
|
if from_line and to_line:
|
28
32
|
self.report_info(
|
29
33
|
tr(
|
30
|
-
"📖 Reading {disp_path} {from_line}-{to_line}",
|
34
|
+
"📖 Reading file '{disp_path}' {from_line}-{to_line}",
|
31
35
|
disp_path=disp_path,
|
32
36
|
from_line=from_line,
|
33
37
|
to_line=to_line,
|
34
38
|
)
|
35
39
|
)
|
36
40
|
else:
|
37
|
-
self.report_info(tr("📖 Reading {disp_path}
|
41
|
+
self.report_info(tr("📖 Reading file '{disp_path}'", disp_path=disp_path))
|
38
42
|
try:
|
39
43
|
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
40
44
|
lines = f.readlines()
|
@@ -59,7 +63,7 @@ class GetLinesTool(ToolBase):
|
|
59
63
|
elif to_line < total_lines:
|
60
64
|
self.report_success(
|
61
65
|
tr(
|
62
|
-
" ✅ {selected_len} {line_word} ({remaining} to
|
66
|
+
" ✅ {selected_len} {line_word} ({remaining} to end)",
|
63
67
|
selected_len=selected_len,
|
64
68
|
line_word=pluralize("line", selected_len),
|
65
69
|
remaining=total_lines - to_line,
|
@@ -68,7 +72,7 @@ class GetLinesTool(ToolBase):
|
|
68
72
|
else:
|
69
73
|
self.report_success(
|
70
74
|
tr(
|
71
|
-
" ✅ {selected_len} {line_word}",
|
75
|
+
" ✅ {selected_len} {line_word} (all)",
|
72
76
|
selected_len=selected_len,
|
73
77
|
line_word=pluralize("line", selected_len),
|
74
78
|
)
|
@@ -98,7 +102,7 @@ class GetLinesTool(ToolBase):
|
|
98
102
|
)
|
99
103
|
else:
|
100
104
|
header = tr(
|
101
|
-
"---\n{disp_path} All lines (total: {total_lines})\n---\n",
|
105
|
+
"---\n{disp_path} All lines (total: {total_lines} (all))\n---\n",
|
102
106
|
disp_path=disp_path,
|
103
107
|
total_lines=total_lines,
|
104
108
|
)
|