janito 2.3.0__py3-none-any.whl → 2.3.1__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 +6 -6
- janito/cli/chat_mode/shell/autocomplete.py +21 -21
- janito/cli/chat_mode/shell/commands/clear.py +12 -12
- janito/cli/chat_mode/shell/commands/multi.py +51 -51
- janito/cli/chat_mode/shell/input_history.py +62 -62
- janito/cli/cli_commands/list_models.py +35 -35
- janito/cli/cli_commands/list_providers.py +9 -9
- janito/cli/cli_commands/list_tools.py +53 -53
- janito/cli/cli_commands/model_selection.py +50 -50
- janito/cli/cli_commands/model_utils.py +95 -95
- janito/cli/cli_commands/set_api_key.py +19 -19
- janito/cli/cli_commands/show_config.py +51 -51
- janito/cli/cli_commands/show_system_prompt.py +62 -62
- janito/cli/core/__init__.py +4 -4
- janito/cli/core/event_logger.py +59 -59
- janito/cli/core/getters.py +33 -33
- janito/cli/core/unsetters.py +54 -54
- janito/cli/single_shot_mode/__init__.py +6 -6
- janito/config.py +5 -5
- janito/config_manager.py +112 -112
- janito/drivers/anthropic/driver.py +113 -113
- janito/formatting_token.py +54 -54
- janito/i18n/__init__.py +35 -35
- janito/i18n/messages.py +23 -23
- janito/i18n/pt.py +47 -47
- janito/llm/__init__.py +5 -5
- janito/llm/agent.py +443 -443
- janito/llm/auth.py +63 -63
- janito/llm/driver_config_builder.py +34 -34
- janito/llm/driver_input.py +12 -12
- janito/llm/message_parts.py +60 -60
- janito/llm/model.py +38 -38
- janito/llm/provider.py +196 -196
- janito/provider_registry.py +176 -176
- janito/providers/anthropic/model_info.py +22 -22
- janito/providers/anthropic/provider.py +2 -0
- janito/providers/azure_openai/model_info.py +16 -16
- janito/providers/azure_openai/provider.py +3 -0
- janito/providers/deepseek/__init__.py +1 -1
- janito/providers/deepseek/model_info.py +16 -16
- janito/providers/deepseek/provider.py +94 -91
- janito/providers/google/provider.py +3 -0
- janito/providers/mistralai/provider.py +3 -0
- janito/providers/openai/provider.py +4 -0
- janito/tools/adapters/__init__.py +1 -1
- janito/tools/adapters/local/ask_user.py +102 -102
- janito/tools/adapters/local/copy_file.py +84 -84
- janito/tools/adapters/local/create_directory.py +69 -69
- janito/tools/adapters/local/create_file.py +82 -82
- janito/tools/adapters/local/fetch_url.py +97 -97
- janito/tools/adapters/local/find_files.py +138 -138
- janito/tools/adapters/local/get_file_outline/__init__.py +1 -1
- janito/tools/adapters/local/get_file_outline/core.py +117 -117
- janito/tools/adapters/local/get_file_outline/java_outline.py +40 -40
- janito/tools/adapters/local/get_file_outline/markdown_outline.py +14 -14
- janito/tools/adapters/local/get_file_outline/python_outline.py +303 -303
- janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -156
- janito/tools/adapters/local/get_file_outline/search_outline.py +33 -33
- janito/tools/adapters/local/python_code_run.py +166 -166
- janito/tools/adapters/local/python_command_run.py +164 -164
- janito/tools/adapters/local/python_file_run.py +163 -163
- janito/tools/adapters/local/run_bash_command.py +176 -176
- janito/tools/adapters/local/run_powershell_command.py +219 -219
- janito/tools/adapters/local/search_text/__init__.py +1 -1
- janito/tools/adapters/local/search_text/core.py +201 -201
- janito/tools/adapters/local/search_text/pattern_utils.py +73 -73
- janito/tools/adapters/local/search_text/traverse_directory.py +145 -145
- janito/tools/adapters/local/validate_file_syntax/__init__.py +1 -1
- janito/tools/adapters/local/validate_file_syntax/core.py +106 -106
- janito/tools/adapters/local/validate_file_syntax/css_validator.py +35 -35
- janito/tools/adapters/local/validate_file_syntax/html_validator.py +93 -93
- janito/tools/adapters/local/validate_file_syntax/js_validator.py +27 -27
- janito/tools/adapters/local/validate_file_syntax/json_validator.py +6 -6
- janito/tools/adapters/local/validate_file_syntax/markdown_validator.py +109 -109
- janito/tools/adapters/local/validate_file_syntax/ps1_validator.py +32 -32
- janito/tools/adapters/local/validate_file_syntax/python_validator.py +5 -5
- janito/tools/adapters/local/validate_file_syntax/xml_validator.py +11 -11
- janito/tools/adapters/local/validate_file_syntax/yaml_validator.py +6 -6
- janito/tools/adapters/local/view_file.py +167 -167
- janito/tools/inspect_registry.py +17 -17
- janito/tools/tool_base.py +105 -105
- janito/tools/tool_events.py +58 -58
- janito/tools/tool_run_exception.py +12 -12
- janito/tools/tool_use_tracker.py +81 -81
- janito/tools/tool_utils.py +45 -45
- janito/tools/tools_schema.py +104 -104
- janito/version.py +4 -4
- {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/METADATA +390 -388
- {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/RECORD +93 -93
- {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/WHEEL +0 -0
- {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/entry_points.txt +0 -0
- {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/licenses/LICENSE +0 -0
- {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/top_level.txt +0 -0
@@ -1,91 +1,94 @@
|
|
1
|
-
from janito.llm.provider import LLMProvider
|
2
|
-
from janito.llm.model import LLMModelInfo
|
3
|
-
from janito.llm.auth import LLMAuthManager
|
4
|
-
from janito.llm.driver_config import LLMDriverConfig
|
5
|
-
from janito.drivers.openai.driver import OpenAIModelDriver
|
6
|
-
from janito.tools import get_local_tools_adapter
|
7
|
-
from janito.providers.registry import LLMProviderRegistry
|
8
|
-
from .model_info import MODEL_SPECS
|
9
|
-
from queue import Queue
|
10
|
-
|
11
|
-
available = OpenAIModelDriver.available
|
12
|
-
unavailable_reason = OpenAIModelDriver.unavailable_reason
|
13
|
-
|
14
|
-
|
15
|
-
class DeepseekProvider(LLMProvider):
|
16
|
-
name = "deepseek"
|
17
|
-
maintainer = "Needs maintainer"
|
18
|
-
MODEL_SPECS = MODEL_SPECS
|
19
|
-
DEFAULT_MODEL = "deepseek-chat" # Options: deepseek-chat, deepseek-reasoner
|
20
|
-
|
21
|
-
def __init__(
|
22
|
-
self, auth_manager: LLMAuthManager = None, config: LLMDriverConfig = None
|
23
|
-
):
|
24
|
-
if
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
self.
|
29
|
-
|
30
|
-
self.
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
if not
|
37
|
-
self._driver_config.
|
38
|
-
|
39
|
-
self.
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
driver
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
)
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
self.
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
1
|
+
from janito.llm.provider import LLMProvider
|
2
|
+
from janito.llm.model import LLMModelInfo
|
3
|
+
from janito.llm.auth import LLMAuthManager
|
4
|
+
from janito.llm.driver_config import LLMDriverConfig
|
5
|
+
from janito.drivers.openai.driver import OpenAIModelDriver
|
6
|
+
from janito.tools import get_local_tools_adapter
|
7
|
+
from janito.providers.registry import LLMProviderRegistry
|
8
|
+
from .model_info import MODEL_SPECS
|
9
|
+
from queue import Queue
|
10
|
+
|
11
|
+
available = OpenAIModelDriver.available
|
12
|
+
unavailable_reason = OpenAIModelDriver.unavailable_reason
|
13
|
+
|
14
|
+
|
15
|
+
class DeepseekProvider(LLMProvider):
|
16
|
+
name = "deepseek"
|
17
|
+
maintainer = "Needs maintainer"
|
18
|
+
MODEL_SPECS = MODEL_SPECS
|
19
|
+
DEFAULT_MODEL = "deepseek-chat" # Options: deepseek-chat, deepseek-reasoner
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self, auth_manager: LLMAuthManager = None, config: LLMDriverConfig = None
|
23
|
+
):
|
24
|
+
# Always set a tools adapter so that even if the driver is unavailable,
|
25
|
+
# generic code paths that expect provider.execute_tool() continue to work.
|
26
|
+
self._tools_adapter = get_local_tools_adapter()
|
27
|
+
if not self.available:
|
28
|
+
self._driver = None
|
29
|
+
else:
|
30
|
+
self.auth_manager = auth_manager or LLMAuthManager()
|
31
|
+
self._api_key = self.auth_manager.get_credentials(type(self).name)
|
32
|
+
self._tools_adapter = get_local_tools_adapter()
|
33
|
+
self._driver_config = config or LLMDriverConfig(model=None)
|
34
|
+
if not self._driver_config.model:
|
35
|
+
self._driver_config.model = self.DEFAULT_MODEL
|
36
|
+
if not self._driver_config.api_key:
|
37
|
+
self._driver_config.api_key = self._api_key
|
38
|
+
# Set DeepSeek public endpoint as default base_url if not provided
|
39
|
+
if not getattr(self._driver_config, "base_url", None):
|
40
|
+
self._driver_config.base_url = "https://api.deepseek.com/v1"
|
41
|
+
self.fill_missing_device_info(self._driver_config)
|
42
|
+
self._driver = None # to be provided by factory/agent
|
43
|
+
|
44
|
+
@property
|
45
|
+
def driver(self) -> OpenAIModelDriver:
|
46
|
+
if not self.available:
|
47
|
+
raise ImportError(f"OpenAIProvider unavailable: {self.unavailable_reason}")
|
48
|
+
return self._driver
|
49
|
+
|
50
|
+
@property
|
51
|
+
def available(self):
|
52
|
+
return available
|
53
|
+
|
54
|
+
@property
|
55
|
+
def unavailable_reason(self):
|
56
|
+
return unavailable_reason
|
57
|
+
|
58
|
+
def create_driver(self):
|
59
|
+
"""
|
60
|
+
Creates and returns a new OpenAIModelDriver instance with input/output queues.
|
61
|
+
"""
|
62
|
+
driver = OpenAIModelDriver(
|
63
|
+
tools_adapter=self._tools_adapter, provider_name=self.name
|
64
|
+
)
|
65
|
+
driver.config = self._driver_config
|
66
|
+
# NOTE: The caller is responsible for calling driver.start() if background processing is needed.
|
67
|
+
return driver
|
68
|
+
|
69
|
+
def create_agent(self, tools_adapter=None, agent_name: str = None, **kwargs):
|
70
|
+
from janito.llm.agent import LLMAgent
|
71
|
+
|
72
|
+
# Always create a new driver with the passed-in tools_adapter
|
73
|
+
if tools_adapter is None:
|
74
|
+
tools_adapter = get_local_tools_adapter()
|
75
|
+
# Should use new-style driver construction via queues/factory (handled elsewhere)
|
76
|
+
raise NotImplementedError(
|
77
|
+
"create_agent must be constructed via new factory using input/output queues and config."
|
78
|
+
)
|
79
|
+
|
80
|
+
@property
|
81
|
+
def model_name(self):
|
82
|
+
return self._driver_config.model
|
83
|
+
|
84
|
+
@property
|
85
|
+
def driver_config(self):
|
86
|
+
"""Public, read-only access to the provider's LLMDriverConfig object."""
|
87
|
+
return self._driver_config
|
88
|
+
|
89
|
+
def execute_tool(self, tool_name: str, event_bus, *args, **kwargs):
|
90
|
+
self._tools_adapter.event_bus = event_bus
|
91
|
+
return self._tools_adapter.execute_by_name(tool_name, *args, **kwargs)
|
92
|
+
|
93
|
+
|
94
|
+
LLMProviderRegistry.register(DeepseekProvider.name, DeepseekProvider)
|
@@ -22,6 +22,9 @@ class GoogleProvider(LLMProvider):
|
|
22
22
|
def __init__(
|
23
23
|
self, auth_manager: LLMAuthManager = None, config: LLMDriverConfig = None
|
24
24
|
):
|
25
|
+
# Always have a tools adapter available to avoid AttributeError downstream when
|
26
|
+
# the driver is missing but other logic still relies on tools execution.
|
27
|
+
self._tools_adapter = get_local_tools_adapter()
|
25
28
|
if not self.available:
|
26
29
|
self._driver = None
|
27
30
|
else:
|
@@ -24,6 +24,9 @@ class MistralAIProvider(LLMProvider):
|
|
24
24
|
def __init__(
|
25
25
|
self, config: LLMDriverConfig = None, auth_manager: LLMAuthManager = None
|
26
26
|
):
|
27
|
+
# Always instantiate a tools adapter so that provider.execute_tool() remains functional
|
28
|
+
# even when the driver cannot be constructed due to missing dependencies.
|
29
|
+
self._tools_adapter = get_local_tools_adapter()
|
27
30
|
if not self.available:
|
28
31
|
self._driver = None
|
29
32
|
return
|
@@ -24,6 +24,10 @@ class OpenAIProvider(LLMProvider):
|
|
24
24
|
self, auth_manager: LLMAuthManager = None, config: LLMDriverConfig = None
|
25
25
|
):
|
26
26
|
if not self.available:
|
27
|
+
# Even when the OpenAI driver is unavailable we still need a tools adapter
|
28
|
+
# so that any generic logic that expects `execute_tool()` to work does not
|
29
|
+
# crash with an AttributeError when it tries to access `self._tools_adapter`.
|
30
|
+
self._tools_adapter = get_local_tools_adapter()
|
27
31
|
self._driver = None
|
28
32
|
else:
|
29
33
|
self.auth_manager = auth_manager or LLMAuthManager()
|
@@ -1 +1 @@
|
|
1
|
-
# Tools providers package: for plug-and-play tool collections, integrations, and adapters.
|
1
|
+
# Tools providers package: for plug-and-play tool collections, integrations, and adapters.
|
@@ -1,102 +1,102 @@
|
|
1
|
-
from janito.tools.tool_base import ToolBase
|
2
|
-
from janito.tools.adapters.local.adapter import register_local_tool
|
3
|
-
|
4
|
-
from rich import print as rich_print
|
5
|
-
from janito.i18n import tr
|
6
|
-
from rich.panel import Panel
|
7
|
-
from prompt_toolkit import PromptSession
|
8
|
-
from prompt_toolkit.key_binding import KeyBindings
|
9
|
-
from prompt_toolkit.enums import EditingMode
|
10
|
-
from prompt_toolkit.formatted_text import HTML
|
11
|
-
from janito.cli.chat_mode.prompt_style import chat_shell_style
|
12
|
-
from prompt_toolkit.styles import Style
|
13
|
-
|
14
|
-
toolbar_style = Style.from_dict({"bottom-toolbar": "fg:yellow bg:darkred"})
|
15
|
-
|
16
|
-
|
17
|
-
@register_local_tool
|
18
|
-
class AskUserTool(ToolBase):
|
19
|
-
"""
|
20
|
-
Prompts the user for clarification or input with a question.
|
21
|
-
|
22
|
-
Args:
|
23
|
-
question (str): The question to ask the user.
|
24
|
-
|
25
|
-
Returns:
|
26
|
-
str: The user's response as a string. Example:
|
27
|
-
- "Yes"
|
28
|
-
- "No"
|
29
|
-
- "Some detailed answer..."
|
30
|
-
"""
|
31
|
-
|
32
|
-
tool_name = "ask_user"
|
33
|
-
|
34
|
-
def run(self, question: str) -> str:
|
35
|
-
|
36
|
-
print() # Print an empty line before the question panel
|
37
|
-
rich_print(Panel.fit(question, title=tr("Question"), style="cyan"))
|
38
|
-
|
39
|
-
bindings = KeyBindings()
|
40
|
-
mode = {"multiline": False}
|
41
|
-
|
42
|
-
@bindings.add("c-r")
|
43
|
-
def _(event):
|
44
|
-
pass
|
45
|
-
|
46
|
-
@bindings.add("f12")
|
47
|
-
def _(event):
|
48
|
-
buf = event.app.current_buffer
|
49
|
-
buf.text = "Do It"
|
50
|
-
buf.validate_and_handle()
|
51
|
-
|
52
|
-
# Use shared CLI styles
|
53
|
-
|
54
|
-
# prompt_style contains the prompt area and input background
|
55
|
-
# toolbar_style contains the bottom-toolbar styling
|
56
|
-
|
57
|
-
# Use the shared chat_shell_style for input styling only
|
58
|
-
style = chat_shell_style
|
59
|
-
|
60
|
-
def get_toolbar():
|
61
|
-
f12_hint = ""
|
62
|
-
if mode["multiline"]:
|
63
|
-
return HTML(
|
64
|
-
f"<b>Multiline mode (Esc+Enter to submit). Type /single to switch.</b>{f12_hint}"
|
65
|
-
)
|
66
|
-
else:
|
67
|
-
return HTML(
|
68
|
-
f"<b>Single-line mode (Enter to submit). Type /multi for multiline.</b>{f12_hint}"
|
69
|
-
)
|
70
|
-
|
71
|
-
session = PromptSession(
|
72
|
-
multiline=False,
|
73
|
-
key_bindings=bindings,
|
74
|
-
editing_mode=EditingMode.EMACS,
|
75
|
-
bottom_toolbar=get_toolbar,
|
76
|
-
style=style,
|
77
|
-
)
|
78
|
-
|
79
|
-
prompt_icon = HTML("<inputline>💬 </inputline>")
|
80
|
-
|
81
|
-
while True:
|
82
|
-
response = session.prompt(prompt_icon)
|
83
|
-
if not mode["multiline"] and response.strip() == "/multi":
|
84
|
-
mode["multiline"] = True
|
85
|
-
session.multiline = True
|
86
|
-
continue
|
87
|
-
elif mode["multiline"] and response.strip() == "/single":
|
88
|
-
mode["multiline"] = False
|
89
|
-
session.multiline = False
|
90
|
-
continue
|
91
|
-
else:
|
92
|
-
sanitized = response.strip()
|
93
|
-
try:
|
94
|
-
sanitized.encode("utf-8")
|
95
|
-
except UnicodeEncodeError:
|
96
|
-
sanitized = sanitized.encode("utf-8", errors="replace").decode(
|
97
|
-
"utf-8"
|
98
|
-
)
|
99
|
-
rich_print(
|
100
|
-
"[yellow]Warning: Some characters in your input were not valid UTF-8 and have been replaced.[/yellow]"
|
101
|
-
)
|
102
|
-
return sanitized
|
1
|
+
from janito.tools.tool_base import ToolBase
|
2
|
+
from janito.tools.adapters.local.adapter import register_local_tool
|
3
|
+
|
4
|
+
from rich import print as rich_print
|
5
|
+
from janito.i18n import tr
|
6
|
+
from rich.panel import Panel
|
7
|
+
from prompt_toolkit import PromptSession
|
8
|
+
from prompt_toolkit.key_binding import KeyBindings
|
9
|
+
from prompt_toolkit.enums import EditingMode
|
10
|
+
from prompt_toolkit.formatted_text import HTML
|
11
|
+
from janito.cli.chat_mode.prompt_style import chat_shell_style
|
12
|
+
from prompt_toolkit.styles import Style
|
13
|
+
|
14
|
+
toolbar_style = Style.from_dict({"bottom-toolbar": "fg:yellow bg:darkred"})
|
15
|
+
|
16
|
+
|
17
|
+
@register_local_tool
|
18
|
+
class AskUserTool(ToolBase):
|
19
|
+
"""
|
20
|
+
Prompts the user for clarification or input with a question.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
question (str): The question to ask the user.
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
str: The user's response as a string. Example:
|
27
|
+
- "Yes"
|
28
|
+
- "No"
|
29
|
+
- "Some detailed answer..."
|
30
|
+
"""
|
31
|
+
|
32
|
+
tool_name = "ask_user"
|
33
|
+
|
34
|
+
def run(self, question: str) -> str:
|
35
|
+
|
36
|
+
print() # Print an empty line before the question panel
|
37
|
+
rich_print(Panel.fit(question, title=tr("Question"), style="cyan"))
|
38
|
+
|
39
|
+
bindings = KeyBindings()
|
40
|
+
mode = {"multiline": False}
|
41
|
+
|
42
|
+
@bindings.add("c-r")
|
43
|
+
def _(event):
|
44
|
+
pass
|
45
|
+
|
46
|
+
@bindings.add("f12")
|
47
|
+
def _(event):
|
48
|
+
buf = event.app.current_buffer
|
49
|
+
buf.text = "Do It"
|
50
|
+
buf.validate_and_handle()
|
51
|
+
|
52
|
+
# Use shared CLI styles
|
53
|
+
|
54
|
+
# prompt_style contains the prompt area and input background
|
55
|
+
# toolbar_style contains the bottom-toolbar styling
|
56
|
+
|
57
|
+
# Use the shared chat_shell_style for input styling only
|
58
|
+
style = chat_shell_style
|
59
|
+
|
60
|
+
def get_toolbar():
|
61
|
+
f12_hint = ""
|
62
|
+
if mode["multiline"]:
|
63
|
+
return HTML(
|
64
|
+
f"<b>Multiline mode (Esc+Enter to submit). Type /single to switch.</b>{f12_hint}"
|
65
|
+
)
|
66
|
+
else:
|
67
|
+
return HTML(
|
68
|
+
f"<b>Single-line mode (Enter to submit). Type /multi for multiline.</b>{f12_hint}"
|
69
|
+
)
|
70
|
+
|
71
|
+
session = PromptSession(
|
72
|
+
multiline=False,
|
73
|
+
key_bindings=bindings,
|
74
|
+
editing_mode=EditingMode.EMACS,
|
75
|
+
bottom_toolbar=get_toolbar,
|
76
|
+
style=style,
|
77
|
+
)
|
78
|
+
|
79
|
+
prompt_icon = HTML("<inputline>💬 </inputline>")
|
80
|
+
|
81
|
+
while True:
|
82
|
+
response = session.prompt(prompt_icon)
|
83
|
+
if not mode["multiline"] and response.strip() == "/multi":
|
84
|
+
mode["multiline"] = True
|
85
|
+
session.multiline = True
|
86
|
+
continue
|
87
|
+
elif mode["multiline"] and response.strip() == "/single":
|
88
|
+
mode["multiline"] = False
|
89
|
+
session.multiline = False
|
90
|
+
continue
|
91
|
+
else:
|
92
|
+
sanitized = response.strip()
|
93
|
+
try:
|
94
|
+
sanitized.encode("utf-8")
|
95
|
+
except UnicodeEncodeError:
|
96
|
+
sanitized = sanitized.encode("utf-8", errors="replace").decode(
|
97
|
+
"utf-8"
|
98
|
+
)
|
99
|
+
rich_print(
|
100
|
+
"[yellow]Warning: Some characters in your input were not valid UTF-8 and have been replaced.[/yellow]"
|
101
|
+
)
|
102
|
+
return sanitized
|
@@ -1,84 +1,84 @@
|
|
1
|
-
import os
|
2
|
-
import shutil
|
3
|
-
from typing import List, Union
|
4
|
-
from janito.tools.adapters.local.adapter import register_local_tool
|
5
|
-
from janito.tools.tool_base import ToolBase
|
6
|
-
from janito.tools.tool_utils import display_path
|
7
|
-
from janito.report_events import ReportAction
|
8
|
-
from janito.i18n import tr
|
9
|
-
|
10
|
-
|
11
|
-
@register_local_tool
|
12
|
-
class CopyFileTool(ToolBase):
|
13
|
-
"""
|
14
|
-
Copy one or more files to a target directory, or copy a single file to a new file.
|
15
|
-
Args:
|
16
|
-
sources (str): Space-separated path(s) to the file(s) to copy.
|
17
|
-
For multiple sources, provide a single string with paths separated by spaces.
|
18
|
-
target (str): Destination path. If copying multiple sources, this must be an existing directory.
|
19
|
-
overwrite (bool, optional): Overwrite existing files. Default: False.
|
20
|
-
Recommended only after reading the file to be overwritten.
|
21
|
-
Returns:
|
22
|
-
str: Status string for each copy operation.
|
23
|
-
"""
|
24
|
-
|
25
|
-
tool_name = "copy_file"
|
26
|
-
|
27
|
-
def run(self, sources: str, target: str, overwrite: bool = False) -> str:
|
28
|
-
source_list = [src for src in sources.split() if src]
|
29
|
-
messages = []
|
30
|
-
if len(source_list) > 1:
|
31
|
-
if not os.path.isdir(target):
|
32
|
-
return tr(
|
33
|
-
"❗ Target must be an existing directory when copying multiple files: '{target}'",
|
34
|
-
target=display_path(target),
|
35
|
-
)
|
36
|
-
for src in source_list:
|
37
|
-
if not os.path.isfile(src):
|
38
|
-
messages.append(
|
39
|
-
tr(
|
40
|
-
"❗ Source file does not exist: '{src}'",
|
41
|
-
src=display_path(src),
|
42
|
-
)
|
43
|
-
)
|
44
|
-
continue
|
45
|
-
dst = os.path.join(target, os.path.basename(src))
|
46
|
-
messages.append(self._copy_one(src, dst, overwrite=overwrite))
|
47
|
-
else:
|
48
|
-
src = source_list[0]
|
49
|
-
if os.path.isdir(target):
|
50
|
-
dst = os.path.join(target, os.path.basename(src))
|
51
|
-
else:
|
52
|
-
dst = target
|
53
|
-
messages.append(self._copy_one(src, dst, overwrite=overwrite))
|
54
|
-
return "\n".join(messages)
|
55
|
-
|
56
|
-
def _copy_one(self, src, dst, overwrite=False) -> str:
|
57
|
-
disp_src = display_path(src)
|
58
|
-
disp_dst = display_path(dst)
|
59
|
-
if not os.path.isfile(src):
|
60
|
-
return tr("❗ Source file does not exist: '{src}'", src=disp_src)
|
61
|
-
if os.path.exists(dst) and not overwrite:
|
62
|
-
return tr(
|
63
|
-
"❗ Target already exists: '{dst}'. Set overwrite=True to replace.",
|
64
|
-
dst=disp_dst,
|
65
|
-
)
|
66
|
-
try:
|
67
|
-
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
68
|
-
shutil.copy2(src, dst)
|
69
|
-
note = (
|
70
|
-
"\n⚠️ Overwrote existing file. (recommended only after reading the file to be overwritten)"
|
71
|
-
if (os.path.exists(dst) and overwrite)
|
72
|
-
else ""
|
73
|
-
)
|
74
|
-
self.report_success(
|
75
|
-
tr("✅ Copied '{src}' to '{dst}'", src=disp_src, dst=disp_dst)
|
76
|
-
)
|
77
|
-
return tr("✅ Copied '{src}' to '{dst}'", src=disp_src, dst=disp_dst) + note
|
78
|
-
except Exception as e:
|
79
|
-
return tr(
|
80
|
-
"❗ Copy failed from '{src}' to '{dst}': {err}",
|
81
|
-
src=disp_src,
|
82
|
-
dst=disp_dst,
|
83
|
-
err=str(e),
|
84
|
-
)
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
from typing import List, Union
|
4
|
+
from janito.tools.adapters.local.adapter import register_local_tool
|
5
|
+
from janito.tools.tool_base import ToolBase
|
6
|
+
from janito.tools.tool_utils import display_path
|
7
|
+
from janito.report_events import ReportAction
|
8
|
+
from janito.i18n import tr
|
9
|
+
|
10
|
+
|
11
|
+
@register_local_tool
|
12
|
+
class CopyFileTool(ToolBase):
|
13
|
+
"""
|
14
|
+
Copy one or more files to a target directory, or copy a single file to a new file.
|
15
|
+
Args:
|
16
|
+
sources (str): Space-separated path(s) to the file(s) to copy.
|
17
|
+
For multiple sources, provide a single string with paths separated by spaces.
|
18
|
+
target (str): Destination path. If copying multiple sources, this must be an existing directory.
|
19
|
+
overwrite (bool, optional): Overwrite existing files. Default: False.
|
20
|
+
Recommended only after reading the file to be overwritten.
|
21
|
+
Returns:
|
22
|
+
str: Status string for each copy operation.
|
23
|
+
"""
|
24
|
+
|
25
|
+
tool_name = "copy_file"
|
26
|
+
|
27
|
+
def run(self, sources: str, target: str, overwrite: bool = False) -> str:
|
28
|
+
source_list = [src for src in sources.split() if src]
|
29
|
+
messages = []
|
30
|
+
if len(source_list) > 1:
|
31
|
+
if not os.path.isdir(target):
|
32
|
+
return tr(
|
33
|
+
"❗ Target must be an existing directory when copying multiple files: '{target}'",
|
34
|
+
target=display_path(target),
|
35
|
+
)
|
36
|
+
for src in source_list:
|
37
|
+
if not os.path.isfile(src):
|
38
|
+
messages.append(
|
39
|
+
tr(
|
40
|
+
"❗ Source file does not exist: '{src}'",
|
41
|
+
src=display_path(src),
|
42
|
+
)
|
43
|
+
)
|
44
|
+
continue
|
45
|
+
dst = os.path.join(target, os.path.basename(src))
|
46
|
+
messages.append(self._copy_one(src, dst, overwrite=overwrite))
|
47
|
+
else:
|
48
|
+
src = source_list[0]
|
49
|
+
if os.path.isdir(target):
|
50
|
+
dst = os.path.join(target, os.path.basename(src))
|
51
|
+
else:
|
52
|
+
dst = target
|
53
|
+
messages.append(self._copy_one(src, dst, overwrite=overwrite))
|
54
|
+
return "\n".join(messages)
|
55
|
+
|
56
|
+
def _copy_one(self, src, dst, overwrite=False) -> str:
|
57
|
+
disp_src = display_path(src)
|
58
|
+
disp_dst = display_path(dst)
|
59
|
+
if not os.path.isfile(src):
|
60
|
+
return tr("❗ Source file does not exist: '{src}'", src=disp_src)
|
61
|
+
if os.path.exists(dst) and not overwrite:
|
62
|
+
return tr(
|
63
|
+
"❗ Target already exists: '{dst}'. Set overwrite=True to replace.",
|
64
|
+
dst=disp_dst,
|
65
|
+
)
|
66
|
+
try:
|
67
|
+
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
68
|
+
shutil.copy2(src, dst)
|
69
|
+
note = (
|
70
|
+
"\n⚠️ Overwrote existing file. (recommended only after reading the file to be overwritten)"
|
71
|
+
if (os.path.exists(dst) and overwrite)
|
72
|
+
else ""
|
73
|
+
)
|
74
|
+
self.report_success(
|
75
|
+
tr("✅ Copied '{src}' to '{dst}'", src=disp_src, dst=disp_dst)
|
76
|
+
)
|
77
|
+
return tr("✅ Copied '{src}' to '{dst}'", src=disp_src, dst=disp_dst) + note
|
78
|
+
except Exception as e:
|
79
|
+
return tr(
|
80
|
+
"❗ Copy failed from '{src}' to '{dst}': {err}",
|
81
|
+
src=disp_src,
|
82
|
+
dst=disp_dst,
|
83
|
+
err=str(e),
|
84
|
+
)
|