janito 1.3.2__tar.gz → 1.4.1__tar.gz
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-1.3.2 → janito-1.4.1}/PKG-INFO +6 -7
- {janito-1.3.2 → janito-1.4.1}/README.md +5 -5
- janito-1.4.1/janito/__init__.py +1 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/conversation.py +7 -1
- {janito-1.3.2 → janito-1.4.1}/janito/agent/templates/system_instructions.j2 +4 -8
- {janito-1.3.2 → janito-1.4.1}/janito/agent/tool_handler.py +62 -95
- janito-1.4.1/janito/agent/tools/__init__.py +9 -0
- janito-1.4.1/janito/agent/tools/ask_user.py +61 -0
- janito-1.4.1/janito/agent/tools/fetch_url.py +35 -0
- janito-1.4.1/janito/agent/tools/file_ops.py +60 -0
- janito-1.4.1/janito/agent/tools/find_files.py +31 -0
- janito-1.4.1/janito/agent/tools/get_file_outline.py +22 -0
- janito-1.4.1/janito/agent/tools/get_lines.py +37 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/tools/gitignore_utils.py +4 -1
- janito-1.4.1/janito/agent/tools/py_compile.py +23 -0
- janito-1.4.1/janito/agent/tools/python_exec.py +54 -0
- janito-1.4.1/janito/agent/tools/remove_directory.py +24 -0
- janito-1.4.1/janito/agent/tools/replace_text_in_file.py +67 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/tools/rich_utils.py +6 -6
- janito-1.4.1/janito/agent/tools/run_bash_command.py +80 -0
- janito-1.4.1/janito/agent/tools/search_files.py +26 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/tools/tool_base.py +4 -2
- janito-1.4.1/janito/agent/tools/utils.py +31 -0
- {janito-1.3.2 → janito-1.4.1}/janito.egg-info/PKG-INFO +6 -7
- {janito-1.3.2 → janito-1.4.1}/janito.egg-info/SOURCES.txt +2 -0
- {janito-1.3.2 → janito-1.4.1}/pyproject.toml +2 -2
- janito-1.3.2/janito/__init__.py +0 -1
- janito-1.3.2/janito/agent/tools/__init__.py +0 -12
- janito-1.3.2/janito/agent/tools/ask_user.py +0 -64
- janito-1.3.2/janito/agent/tools/fetch_url.py +0 -40
- janito-1.3.2/janito/agent/tools/file_ops.py +0 -72
- janito-1.3.2/janito/agent/tools/find_files.py +0 -58
- janito-1.3.2/janito/agent/tools/get_lines.py +0 -58
- janito-1.3.2/janito/agent/tools/py_compile.py +0 -26
- janito-1.3.2/janito/agent/tools/python_exec.py +0 -47
- janito-1.3.2/janito/agent/tools/remove_directory.py +0 -38
- janito-1.3.2/janito/agent/tools/replace_text_in_file.py +0 -67
- janito-1.3.2/janito/agent/tools/run_bash_command.py +0 -134
- janito-1.3.2/janito/agent/tools/search_files.py +0 -52
- {janito-1.3.2 → janito-1.4.1}/LICENSE +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/__main__.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/__init__.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/agent.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/config.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/config_defaults.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/config_utils.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/queued_tool_handler.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/runtime_config.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/agent/tools/rich_live.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli/__init__.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli/_print_config.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli/_utils.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli/arg_parser.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli/config_commands.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli/logging_setup.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli/main.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli/runner.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/__init__.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/chat_loop.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/commands.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/config_shell.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/load_prompt.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/session_manager.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/ui.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/render_prompt.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/web/__init__.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/web/__main__.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito/web/app.py +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito.egg-info/dependency_links.txt +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito.egg-info/entry_points.txt +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito.egg-info/requires.txt +0 -0
- {janito-1.3.2 → janito-1.4.1}/janito.egg-info/top_level.txt +0 -0
- {janito-1.3.2 → janito-1.4.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: janito
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.4.1
|
4
4
|
Summary: A Natural Programming Language Agent,
|
5
5
|
Author-email: João Pinto <joao.pinto@gmail.com>
|
6
6
|
License: MIT
|
@@ -8,7 +8,6 @@ Project-URL: homepage, https://github.com/joaompinto/janito
|
|
8
8
|
Project-URL: repository, https://github.com/joaompinto/janito
|
9
9
|
Keywords: agent,framework,tools,automation
|
10
10
|
Classifier: Programming Language :: Python :: 3.10
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
12
11
|
Classifier: Operating System :: OS Independent
|
13
12
|
Requires-Python: >=3.10
|
14
13
|
Description-Content-Type: text/markdown
|
@@ -23,7 +22,7 @@ Requires-Dist: requests
|
|
23
22
|
Requires-Dist: rich
|
24
23
|
Dynamic: license-file
|
25
24
|
|
26
|
-
# 🚀 Janito: Agent
|
25
|
+
# 🚀 Janito: Natural Programming Language Agent
|
27
26
|
|
28
27
|
Janito is an AI-powered assistant for the command line and web that interprets natural language instructions to edit code, manage files, and analyze projects using patterns and tools designed by experienced software engineers. It prioritizes transparency, interactive clarification, and precise, reviewable changes.
|
29
28
|
|
@@ -60,7 +59,7 @@ python -m janito.web
|
|
60
59
|
- `replace_text_in_file`: Replace exact text fragments in files.
|
61
60
|
- `search_files`: Search for text patterns across files.
|
62
61
|
- `python_exec`: Execute Python code and capture output.
|
63
|
-
- And more
|
62
|
+
- And more built-in operations for code and file management.
|
64
63
|
- 🌐 **Web Interface (In Development):** Upcoming simple web UI for streaming responses and tool progress.
|
65
64
|
|
66
65
|
---
|
@@ -79,9 +78,9 @@ Below are the supported configuration parameters and CLI flags. Some options can
|
|
79
78
|
|
80
79
|
| Key / Flag | Description | How to set | Default |
|
81
80
|
|---------------------------|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------|
|
82
|
-
| `api_key` | API key for
|
83
|
-
| `model` | Model name to use for this session | `--model` (session only), `--set-local-config model=...`, or `--set-global-config` |
|
84
|
-
| `base_url` | API base URL
|
81
|
+
| `api_key` | API key for a compatible language model service | `--set-api-key`, config file | _None_ (required) |
|
82
|
+
| `model` | Model name to use for this session | `--model` (session only), `--set-local-config model=...`, or `--set-global-config` | _(example: gpt-4)_ |
|
83
|
+
| `base_url` | API base URL for your language model service | `--set-local-config base_url=...` or `--set-global-config` | _(example: https://api.your-model.com)_ |
|
85
84
|
| `role` | Role description for the system prompt | `--role` or config | "software engineer" |
|
86
85
|
| `system_prompt` | Override the entire system prompt as a raw string | `--system-prompt` or config | _Default prompt_ |
|
87
86
|
| `system_file` | Use a plain text file as the system prompt (takes precedence over `system_prompt`) | `--system-file` (CLI only) | _None_ |
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# 🚀 Janito: Agent
|
1
|
+
# 🚀 Janito: Natural Programming Language Agent
|
2
2
|
|
3
3
|
Janito is an AI-powered assistant for the command line and web that interprets natural language instructions to edit code, manage files, and analyze projects using patterns and tools designed by experienced software engineers. It prioritizes transparency, interactive clarification, and precise, reviewable changes.
|
4
4
|
|
@@ -35,7 +35,7 @@ python -m janito.web
|
|
35
35
|
- `replace_text_in_file`: Replace exact text fragments in files.
|
36
36
|
- `search_files`: Search for text patterns across files.
|
37
37
|
- `python_exec`: Execute Python code and capture output.
|
38
|
-
- And more
|
38
|
+
- And more built-in operations for code and file management.
|
39
39
|
- 🌐 **Web Interface (In Development):** Upcoming simple web UI for streaming responses and tool progress.
|
40
40
|
|
41
41
|
---
|
@@ -54,9 +54,9 @@ Below are the supported configuration parameters and CLI flags. Some options can
|
|
54
54
|
|
55
55
|
| Key / Flag | Description | How to set | Default |
|
56
56
|
|---------------------------|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------|
|
57
|
-
| `api_key` | API key for
|
58
|
-
| `model` | Model name to use for this session | `--model` (session only), `--set-local-config model=...`, or `--set-global-config` |
|
59
|
-
| `base_url` | API base URL
|
57
|
+
| `api_key` | API key for a compatible language model service | `--set-api-key`, config file | _None_ (required) |
|
58
|
+
| `model` | Model name to use for this session | `--model` (session only), `--set-local-config model=...`, or `--set-global-config` | _(example: gpt-4)_ |
|
59
|
+
| `base_url` | API base URL for your language model service | `--set-local-config base_url=...` or `--set-global-config` | _(example: https://api.your-model.com)_ |
|
60
60
|
| `role` | Role description for the system prompt | `--role` or config | "software engineer" |
|
61
61
|
| `system_prompt` | Override the entire system prompt as a raw string | `--system-prompt` or config | _Default prompt_ |
|
62
62
|
| `system_file` | Use a plain text file as the system prompt (takes precedence over `system_prompt`) | `--system-file` (CLI only) | _None_ |
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "1.4.1"
|
@@ -45,7 +45,13 @@ class ConversationHandler:
|
|
45
45
|
if spinner:
|
46
46
|
# Calculate word count for all messages
|
47
47
|
word_count = sum(len(str(m.get('content', '')).split()) for m in messages if 'content' in m)
|
48
|
-
|
48
|
+
def format_count(n):
|
49
|
+
if n >= 1_000_000:
|
50
|
+
return f"{n/1_000_000:.1f}m"
|
51
|
+
elif n >= 1_000:
|
52
|
+
return f"{n/1_000:.1f}k"
|
53
|
+
return str(n)
|
54
|
+
spinner_msg = f"[bold green]Waiting for AI response... ({format_count(word_count)} words in conversation)"
|
49
55
|
with console.status(spinner_msg, spinner="dots") as status:
|
50
56
|
response = self.client.chat.completions.create(
|
51
57
|
model=self.model,
|
@@ -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>
|
@@ -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"
|
@@ -0,0 +1,9 @@
|
|
1
|
+
import importlib
|
2
|
+
import os
|
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}')
|
@@ -0,0 +1,61 @@
|
|
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
|
58
|
+
|
59
|
+
|
60
|
+
from janito.agent.tool_handler import ToolHandler
|
61
|
+
ToolHandler.register_tool(AskUserTool, name="ask_user")
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import requests
|
2
|
+
from typing import Optional
|
3
|
+
from bs4 import BeautifulSoup
|
4
|
+
from janito.agent.tool_handler import ToolHandler
|
5
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
6
|
+
from janito.agent.tools.tool_base import ToolBase
|
7
|
+
|
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} ... ", end="")
|
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')
|
17
|
+
|
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."
|
31
|
+
|
32
|
+
print_success("✅ Success")
|
33
|
+
return text
|
34
|
+
|
35
|
+
ToolHandler.register_tool(FetchUrlTool, name="fetch_url")
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
from janito.agent.tool_handler import ToolHandler
|
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
|
7
|
+
|
8
|
+
class CreateFileTool(ToolBase):
|
9
|
+
"""
|
10
|
+
Create a new file or update an existing file with the given content.
|
11
|
+
"""
|
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
|
+
new_lines = content.count('\n') + 1 if content else 0
|
35
|
+
if old_lines is not None:
|
36
|
+
print_success(f"✅ Successfully updated the file at '{disp_path}' ({old_lines} > {new_lines} lines).")
|
37
|
+
return f"✅ Successfully updated the file at '{disp_path}' ({old_lines} > {new_lines} lines)."
|
38
|
+
print_success(f"✅ Successfully created the file at '{disp_path}' ({new_lines} lines).")
|
39
|
+
return f"✅ Successfully created the file at '{disp_path}' ({new_lines} lines)."
|
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."
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from janito.agent.tools.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_handler import ToolHandler
|
3
|
+
from janito.agent.tools.rich_utils import print_info, print_success
|
4
|
+
import os
|
5
|
+
import fnmatch
|
6
|
+
|
7
|
+
class FindFilesTool(ToolBase):
|
8
|
+
"""Find files in a directory matching a pattern."""
|
9
|
+
def call(self, directory: str, pattern: str, recursive: bool=False, max_results: int=100) -> str:
|
10
|
+
import os
|
11
|
+
def _display_path(path):
|
12
|
+
import os
|
13
|
+
if os.path.isabs(path):
|
14
|
+
return path
|
15
|
+
return os.path.relpath(path)
|
16
|
+
disp_path = _display_path(directory)
|
17
|
+
rec = "recursively" if recursive else "non-recursively"
|
18
|
+
print_info(f"\U0001F50D Searching '{disp_path}' for pattern '{pattern}' ({rec}, max {max_results})", end="")
|
19
|
+
self.update_progress(f"Searching for files in {directory} matching {pattern}")
|
20
|
+
matches = []
|
21
|
+
for root, dirs, files in os.walk(directory):
|
22
|
+
for filename in fnmatch.filter(files, pattern):
|
23
|
+
matches.append(os.path.join(root, filename))
|
24
|
+
if len(matches) >= max_results:
|
25
|
+
break
|
26
|
+
if not recursive:
|
27
|
+
break
|
28
|
+
print_success(f"✅ {len(matches)} found")
|
29
|
+
return "\n".join(matches)
|
30
|
+
|
31
|
+
ToolHandler.register_tool(FindFilesTool, name="find_files")
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from janito.agent.tools.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_handler import ToolHandler
|
3
|
+
|
4
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
5
|
+
|
6
|
+
class GetFileOutlineTool(ToolBase):
|
7
|
+
"""Get an outline of a file's structure."""
|
8
|
+
def call(self, file_path: str) -> str:
|
9
|
+
print_info(f"\U0001F4C4 Getting outline for: {file_path}", end="")
|
10
|
+
self.update_progress(f"Getting outline for: {file_path}")
|
11
|
+
try:
|
12
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
13
|
+
lines = f.readlines()
|
14
|
+
outline = [line.strip() for line in lines if line.strip()]
|
15
|
+
num_items = len(outline)
|
16
|
+
print_success(f"✅ Outline generated ({num_items} items)")
|
17
|
+
return f"Outline: {num_items} items\n" + '\n'.join(outline)
|
18
|
+
except Exception as e:
|
19
|
+
print_error(f"\u274c Error reading file: {e}")
|
20
|
+
return f"Error reading file: {e}"
|
21
|
+
|
22
|
+
ToolHandler.register_tool(GetFileOutlineTool, name="get_file_outline")
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from janito.agent.tools.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_handler import ToolHandler
|
3
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
4
|
+
|
5
|
+
class GetLinesTool(ToolBase):
|
6
|
+
"""Get specific lines from a file."""
|
7
|
+
def call(self, file_path: str, from_line: int=None, to_line: int=None) -> str:
|
8
|
+
import os
|
9
|
+
def _display_path(path):
|
10
|
+
import os
|
11
|
+
if os.path.isabs(path):
|
12
|
+
return path
|
13
|
+
return os.path.relpath(path)
|
14
|
+
disp_path = _display_path(file_path)
|
15
|
+
if from_line and to_line:
|
16
|
+
count = to_line - from_line + 1
|
17
|
+
print_info(f"📄 Reading {disp_path}:{from_line} ({count} lines)", end="")
|
18
|
+
else:
|
19
|
+
print_info(f"📄 Reading {disp_path} (all lines)", end="")
|
20
|
+
self.update_progress(f"Getting lines {from_line} to {to_line} from {file_path}")
|
21
|
+
try:
|
22
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
23
|
+
lines = f.readlines()
|
24
|
+
selected = lines[(from_line-1 if from_line else 0):(to_line if to_line else None)]
|
25
|
+
if from_line and to_line:
|
26
|
+
print_success(f" ✅ {to_line - from_line + 1} lines read")
|
27
|
+
else:
|
28
|
+
print_success(f" ✅ {len(lines)} lines read")
|
29
|
+
return ''.join(selected)
|
30
|
+
except Exception as e:
|
31
|
+
if isinstance(e, FileNotFoundError):
|
32
|
+
print_error(f"❗ not found")
|
33
|
+
return "❗ not found"
|
34
|
+
print_error(f" ❌ Error: {e}")
|
35
|
+
return f"Error reading file: {e}"
|
36
|
+
|
37
|
+
ToolHandler.register_tool(GetLinesTool, name="get_lines")
|
@@ -1,15 +1,17 @@
|
|
1
1
|
import os
|
2
2
|
import pathspec
|
3
|
+
from janito.agent.tools.utils import expand_path
|
3
4
|
|
4
5
|
_spec = None
|
5
6
|
|
6
7
|
|
7
8
|
def load_gitignore_patterns(gitignore_path='.gitignore'):
|
8
9
|
global _spec
|
10
|
+
gitignore_path = expand_path(gitignore_path)
|
9
11
|
if not os.path.exists(gitignore_path):
|
10
12
|
_spec = pathspec.PathSpec.from_lines('gitwildmatch', [])
|
11
13
|
return _spec
|
12
|
-
with open(gitignore_path, 'r') as f:
|
14
|
+
with open(gitignore_path, 'r', encoding='utf-8') as f:
|
13
15
|
lines = f.readlines()
|
14
16
|
_spec = pathspec.PathSpec.from_lines('gitwildmatch', lines)
|
15
17
|
return _spec
|
@@ -17,6 +19,7 @@ def load_gitignore_patterns(gitignore_path='.gitignore'):
|
|
17
19
|
|
18
20
|
def is_ignored(path):
|
19
21
|
global _spec
|
22
|
+
path = expand_path(path)
|
20
23
|
if _spec is None:
|
21
24
|
_spec = load_gitignore_patterns()
|
22
25
|
# Normalize path to be relative and use forward slashes
|
@@ -0,0 +1,23 @@
|
|
1
|
+
from janito.agent.tools.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_handler import ToolHandler
|
3
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
4
|
+
from typing import Optional
|
5
|
+
import py_compile
|
6
|
+
|
7
|
+
class PyCompileTool(ToolBase):
|
8
|
+
"""Validate a Python file by compiling it with py_compile."""
|
9
|
+
def call(self, file_path: str, doraise: Optional[bool] = True) -> str:
|
10
|
+
print_info(f"[py_compile] Compiling Python file: {file_path}", end="")
|
11
|
+
self.update_progress(f"Compiling Python file: {file_path}")
|
12
|
+
try:
|
13
|
+
py_compile.compile(file_path, doraise=doraise)
|
14
|
+
print_success(f"[py_compile] Compiled successfully: {file_path}")
|
15
|
+
return f"Compiled successfully: {file_path}"
|
16
|
+
except py_compile.PyCompileError as e:
|
17
|
+
print_error(f"[py_compile] Compile error: {e}")
|
18
|
+
return f"Compile error: {e}"
|
19
|
+
except Exception as e:
|
20
|
+
print_error(f"[py_compile] Error: {e}")
|
21
|
+
return f"Error: {e}"
|
22
|
+
|
23
|
+
ToolHandler.register_tool(PyCompileTool, name="py_compile_file")
|