janito 1.5.2__py3-none-any.whl → 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. janito/__init__.py +1 -1
  2. janito/__main__.py +0 -1
  3. janito/agent/config.py +11 -10
  4. janito/agent/config_defaults.py +3 -2
  5. janito/agent/conversation.py +93 -119
  6. janito/agent/conversation_api.py +98 -0
  7. janito/agent/conversation_exceptions.py +12 -0
  8. janito/agent/conversation_tool_calls.py +22 -0
  9. janito/agent/conversation_ui.py +17 -0
  10. janito/agent/message_handler.py +8 -9
  11. janito/agent/{agent.py → openai_client.py} +48 -16
  12. janito/agent/openai_schema_generator.py +53 -37
  13. janito/agent/profile_manager.py +172 -0
  14. janito/agent/queued_message_handler.py +13 -14
  15. janito/agent/rich_live.py +32 -0
  16. janito/agent/rich_message_handler.py +64 -0
  17. janito/agent/runtime_config.py +6 -1
  18. janito/agent/{tools/tool_base.py → tool_base.py} +15 -8
  19. janito/agent/tool_registry.py +118 -132
  20. janito/agent/tools/__init__.py +41 -2
  21. janito/agent/tools/ask_user.py +43 -33
  22. janito/agent/tools/create_directory.py +18 -16
  23. janito/agent/tools/create_file.py +31 -36
  24. janito/agent/tools/fetch_url.py +23 -19
  25. janito/agent/tools/find_files.py +40 -36
  26. janito/agent/tools/get_file_outline.py +100 -22
  27. janito/agent/tools/get_lines.py +40 -32
  28. janito/agent/tools/gitignore_utils.py +9 -6
  29. janito/agent/tools/move_file.py +22 -13
  30. janito/agent/tools/py_compile_file.py +40 -0
  31. janito/agent/tools/remove_directory.py +34 -24
  32. janito/agent/tools/remove_file.py +22 -20
  33. janito/agent/tools/replace_file.py +51 -0
  34. janito/agent/tools/replace_text_in_file.py +69 -42
  35. janito/agent/tools/rich_live.py +9 -2
  36. janito/agent/tools/run_bash_command.py +155 -107
  37. janito/agent/tools/run_python_command.py +139 -0
  38. janito/agent/tools/search_files.py +51 -34
  39. janito/agent/tools/tools_utils.py +4 -2
  40. janito/agent/tools/utils.py +6 -2
  41. janito/cli/_print_config.py +42 -16
  42. janito/cli/_utils.py +1 -0
  43. janito/cli/arg_parser.py +182 -29
  44. janito/cli/config_commands.py +54 -22
  45. janito/cli/logging_setup.py +9 -3
  46. janito/cli/main.py +11 -10
  47. janito/cli/runner/__init__.py +2 -0
  48. janito/cli/runner/cli_main.py +148 -0
  49. janito/cli/runner/config.py +33 -0
  50. janito/cli/runner/formatting.py +12 -0
  51. janito/cli/runner/scan.py +44 -0
  52. janito/cli_chat_shell/__init__.py +0 -1
  53. janito/cli_chat_shell/chat_loop.py +71 -92
  54. janito/cli_chat_shell/chat_state.py +38 -0
  55. janito/cli_chat_shell/chat_ui.py +43 -0
  56. janito/cli_chat_shell/commands/__init__.py +45 -0
  57. janito/cli_chat_shell/commands/config.py +22 -0
  58. janito/cli_chat_shell/commands/history_reset.py +29 -0
  59. janito/cli_chat_shell/commands/session.py +48 -0
  60. janito/cli_chat_shell/commands/session_control.py +12 -0
  61. janito/cli_chat_shell/commands/system.py +73 -0
  62. janito/cli_chat_shell/commands/utility.py +29 -0
  63. janito/cli_chat_shell/config_shell.py +39 -10
  64. janito/cli_chat_shell/load_prompt.py +5 -2
  65. janito/cli_chat_shell/session_manager.py +24 -27
  66. janito/cli_chat_shell/ui.py +75 -40
  67. janito/rich_utils.py +15 -2
  68. janito/web/__main__.py +10 -2
  69. janito/web/app.py +88 -52
  70. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/METADATA +76 -11
  71. janito-1.6.0.dist-info/RECORD +81 -0
  72. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/WHEEL +1 -1
  73. janito/agent/rich_tool_handler.py +0 -43
  74. janito/agent/templates/system_instructions.j2 +0 -38
  75. janito/agent/tool_auto_imports.py +0 -5
  76. janito/agent/tools/append_text_to_file.py +0 -41
  77. janito/agent/tools/py_compile.py +0 -39
  78. janito/agent/tools/python_exec.py +0 -83
  79. janito/cli/runner.py +0 -137
  80. janito/cli_chat_shell/commands.py +0 -204
  81. janito/render_prompt.py +0 -13
  82. janito-1.5.2.dist-info/RECORD +0 -66
  83. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/entry_points.txt +0 -0
  84. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/licenses/LICENSE +0 -0
  85. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/top_level.txt +0 -0
@@ -1,132 +1,118 @@
1
- # janito/agent/tool_registry.py
2
- import json
3
- from janito.agent.tools.tool_base import ToolBase
4
- from janito.agent.openai_schema_generator import generate_openai_function_schema
5
-
6
- _tool_registry = {}
7
-
8
- def register_tool(tool=None, *, name: str = None):
9
- if tool is None:
10
- return lambda t: register_tool(t, name=name)
11
- override_name = name
12
- if not (isinstance(tool, type) and issubclass(tool, ToolBase)):
13
- raise TypeError("Tool must be a class derived from ToolBase.")
14
- instance = tool()
15
- func = instance.call
16
- default_name = tool.__name__
17
- tool_name = override_name or default_name
18
- schema = generate_openai_function_schema(func, tool_name)
19
- _tool_registry[tool_name] = {
20
- "function": func,
21
- "description": schema["description"],
22
- "parameters": schema["parameters"]
23
- }
24
- return tool
25
-
26
- def get_tool_schemas():
27
- schemas = []
28
- for name, entry in _tool_registry.items():
29
- schemas.append({
30
- "type": "function",
31
- "function": {
32
- "name": name,
33
- "description": entry["description"],
34
- "parameters": entry["parameters"]
35
- }
36
- })
37
- return schemas
38
-
39
- def handle_tool_call(tool_call, message_handler=None, verbose=False):
40
- import uuid
41
- call_id = getattr(tool_call, 'id', None) or str(uuid.uuid4())
42
- tool_entry = _tool_registry.get(tool_call.function.name)
43
- if not tool_entry:
44
- return f"Unknown tool: {tool_call.function.name}"
45
- func = tool_entry["function"]
46
- args = json.loads(tool_call.function.arguments)
47
- if verbose:
48
- print(f"[Tool Call] {tool_call.function.name} called with arguments: {args}")
49
- instance = None
50
- if hasattr(func, '__self__') and isinstance(func.__self__, ToolBase):
51
- instance = func.__self__
52
- if message_handler:
53
- instance._progress_callback = message_handler.handle_message
54
- # Emit tool_call event before calling the tool
55
- if message_handler:
56
- message_handler.handle_message({
57
- 'type': 'tool_call',
58
- 'tool': tool_call.function.name,
59
- 'call_id': call_id,
60
- 'arguments': args,
61
- })
62
- try:
63
- result = func(**args)
64
- if message_handler:
65
- message_handler.handle_message({
66
- 'type': 'tool_result',
67
- 'tool': tool_call.function.name,
68
- 'call_id': call_id,
69
- 'result': result,
70
- })
71
- return result
72
- except Exception as e:
73
- if message_handler:
74
- message_handler.handle_message({
75
- 'type': 'tool_error',
76
- 'tool': tool_call.function.name,
77
- 'call_id': call_id,
78
- 'error': str(e),
79
- })
80
- raise
81
-
82
- def handle_tool_call(tool_call, message_handler=None, verbose=False):
83
- import uuid
84
- call_id = getattr(tool_call, 'id', None) or str(uuid.uuid4())
85
- tool_entry = _tool_registry.get(tool_call.function.name)
86
- if not tool_entry:
87
- return f"Unknown tool: {tool_call.function.name}"
88
- func = tool_entry["function"]
89
- args = json.loads(tool_call.function.arguments)
90
- if verbose:
91
- print(f"[Tool Call] {tool_call.function.name} called with arguments: {args}")
92
- instance = None
93
- if hasattr(func, '__self__') and isinstance(func.__self__, ToolBase):
94
- instance = func.__self__
95
- if message_handler:
96
- instance._progress_callback = message_handler.handle_message
97
- # Emit tool_call event before calling the tool
98
- if message_handler:
99
- message_handler.handle_message({
100
- 'type': 'tool_call',
101
- 'tool': tool_call.function.name,
102
- 'args': args,
103
- 'call_id': call_id
104
- })
105
- try:
106
- result = func(**args)
107
- except Exception as e:
108
- import traceback # Kept here: only needed on error
109
- error_message = f"[Tool Error] {type(e).__name__}: {e}\n" + traceback.format_exc()
110
- if message_handler:
111
- message_handler.handle_message({'type': 'error', 'message': error_message})
112
- result = error_message
113
- # Emit tool_result event after tool execution
114
- if message_handler:
115
- message_handler.handle_message({
116
- 'type': 'tool_result',
117
- 'tool': tool_call.function.name,
118
- 'call_id': call_id,
119
- 'result': result
120
- })
121
- if verbose:
122
- preview = result
123
- if isinstance(result, str):
124
- lines = result.splitlines()
125
- if len(lines) > 10:
126
- preview = "\n".join(lines[:10]) + "\n... (truncated)"
127
- elif len(result) > 500:
128
- preview = result[:500] + "... (truncated)"
129
- print(f"[Tool Result] {tool_call.function.name} returned:\n{preview}")
130
- if instance is not None:
131
- instance._progress_callback = None
132
- return result
1
+ # janito/agent/tool_registry.py
2
+ import json
3
+ from janito.agent.tool_base import ToolBase
4
+ from janito.agent.openai_schema_generator import generate_openai_function_schema
5
+ import inspect
6
+
7
+ _tool_registry = {}
8
+
9
+
10
+ def register_tool(tool=None, *, name: str = None):
11
+ if tool is None:
12
+ return lambda t: register_tool(t, name=name)
13
+ override_name = name
14
+ if not (isinstance(tool, type) and issubclass(tool, ToolBase)):
15
+ raise TypeError("Tool must be a class derived from ToolBase.")
16
+ instance = tool()
17
+ if not hasattr(instance, "call") or not callable(instance.call):
18
+ raise TypeError(
19
+ f"Tool '{tool.__name__}' must implement a callable 'call' method."
20
+ )
21
+ tool_name = override_name or instance.name
22
+ if tool_name in _tool_registry:
23
+ raise ValueError(f"Tool '{tool_name}' is already registered.")
24
+ schema = generate_openai_function_schema(instance.call, tool_name, tool_class=tool)
25
+ _tool_registry[tool_name] = {
26
+ "function": instance.call,
27
+ "description": schema["description"],
28
+ "parameters": schema["parameters"],
29
+ "class": tool,
30
+ "instance": instance,
31
+ }
32
+ return tool
33
+
34
+
35
+ def get_tool_schemas():
36
+ schemas = []
37
+ for name, entry in _tool_registry.items():
38
+ schemas.append(
39
+ {
40
+ "type": "function",
41
+ "function": {
42
+ "name": name,
43
+ "description": entry["description"],
44
+ "parameters": entry["parameters"],
45
+ },
46
+ }
47
+ )
48
+ return schemas
49
+
50
+
51
+ def handle_tool_call(tool_call, message_handler=None, verbose=False):
52
+ import uuid
53
+
54
+ call_id = getattr(tool_call, "id", None) or str(uuid.uuid4())
55
+ tool_entry = _tool_registry.get(tool_call.function.name)
56
+ if not tool_entry:
57
+ return f"Unknown tool: {tool_call.function.name}"
58
+ func = tool_entry["function"]
59
+ args = json.loads(tool_call.function.arguments)
60
+ if verbose:
61
+ print(f"[Tool Call] {tool_call.function.name} called with arguments: {args}")
62
+ instance = None
63
+ if hasattr(func, "__self__") and isinstance(func.__self__, ToolBase):
64
+ instance = func.__self__
65
+ if message_handler:
66
+ instance._progress_callback = message_handler.handle_message
67
+ # Emit tool_call event before calling the tool
68
+ if message_handler:
69
+ message_handler.handle_message(
70
+ {
71
+ "type": "tool_call",
72
+ "tool": tool_call.function.name,
73
+ "call_id": call_id,
74
+ "arguments": args,
75
+ }
76
+ )
77
+ # --- Argument validation start ---
78
+ sig = inspect.signature(func)
79
+ try:
80
+ sig.bind(**args)
81
+ except TypeError as e:
82
+ error_msg = (
83
+ f"Argument validation error for tool '{tool_call.function.name}': {str(e)}"
84
+ )
85
+ if message_handler:
86
+ message_handler.handle_message(
87
+ {
88
+ "type": "tool_error",
89
+ "tool": tool_call.function.name,
90
+ "call_id": call_id,
91
+ "error": error_msg,
92
+ }
93
+ )
94
+ raise TypeError(error_msg)
95
+ # --- Argument validation end ---
96
+ try:
97
+ result = func(**args)
98
+ if message_handler:
99
+ message_handler.handle_message(
100
+ {
101
+ "type": "tool_result",
102
+ "tool": tool_call.function.name,
103
+ "call_id": call_id,
104
+ "result": result,
105
+ }
106
+ )
107
+ return result
108
+ except Exception as e:
109
+ if message_handler:
110
+ message_handler.handle_message(
111
+ {
112
+ "type": "tool_error",
113
+ "tool": tool_call.function.name,
114
+ "call_id": call_id,
115
+ "error": str(e),
116
+ }
117
+ )
118
+ raise
@@ -1,2 +1,41 @@
1
- # This file is intentionally empty.
2
- # Tool auto-imports are handled by janito.agent.tool_auto_imports at CLI startup.
1
+ from . import ask_user
2
+ from . import create_directory
3
+ from . import create_file
4
+ from . import fetch_url
5
+ from . import find_files
6
+ from . import get_file_outline
7
+ from . import get_lines
8
+ from . import gitignore_utils
9
+ from . import move_file
10
+ from . import py_compile_file
11
+ from . import remove_directory
12
+ from . import remove_file
13
+ from . import replace_text_in_file
14
+ from . import rich_live
15
+ from . import run_bash_command
16
+ from . import run_python_command
17
+ from . import search_files
18
+ from . import tools_utils
19
+ from . import replace_file
20
+
21
+ __all__ = [
22
+ "ask_user",
23
+ "create_directory",
24
+ "create_file",
25
+ "fetch_url",
26
+ "find_files",
27
+ "get_file_outline",
28
+ "get_lines",
29
+ "gitignore_utils",
30
+ "move_file",
31
+ "py_compile_file",
32
+ "remove_directory",
33
+ "remove_file",
34
+ "replace_text_in_file",
35
+ "rich_live",
36
+ "run_bash_command",
37
+ "run_python_command",
38
+ "search_files",
39
+ "tools_utils",
40
+ "replace_file",
41
+ ]
@@ -1,23 +1,22 @@
1
- from janito.agent.tools.tool_base import ToolBase
1
+ from janito.agent.tool_base import ToolBase
2
2
  from janito.agent.tool_registry import register_tool
3
3
 
4
+
4
5
  @register_tool(name="ask_user")
5
6
  class AskUserTool(ToolBase):
6
- """Ask the user a question and return their response."""
7
- def call(self, question: str) -> str:
8
- """
9
- Ask the user a question and return their response.
10
-
11
- Args:
12
- question (str): The question to ask the user.
7
+ """
8
+ Request clarification or input from the user whenever there is uncertainty, ambiguity, missing information, or multiple valid options. Returns the user's response as a string.
13
9
 
14
- Returns:
15
- str: The user's response as a string. Example:
16
- - "Yes"
17
- - "No"
18
- - "Some detailed answer..."
19
- """
10
+ Args:
11
+ question (str): The question to ask the user.
12
+ Returns:
13
+ str: The user's response as a string. Example:
14
+ - "Yes"
15
+ - "No"
16
+ - "Some detailed answer..."
17
+ """
20
18
 
19
+ def call(self, question: str) -> str:
21
20
  from rich import print as rich_print
22
21
  from rich.panel import Panel
23
22
  from prompt_toolkit import PromptSession
@@ -29,46 +28,57 @@ class AskUserTool(ToolBase):
29
28
  rich_print(Panel.fit(question, title="Question", style="cyan"))
30
29
 
31
30
  bindings = KeyBindings()
32
- mode = {'multiline': False}
31
+ mode = {"multiline": False}
33
32
 
34
- @bindings.add('c-r')
33
+ @bindings.add("c-r")
35
34
  def _(event):
36
35
  pass
37
36
 
38
- style = Style.from_dict({
39
- 'bottom-toolbar': 'bg:#333333 #ffffff',
40
- 'b': 'bold',
41
- 'prompt': 'bold bg:#000080 #ffffff',
42
- })
37
+ @bindings.add("f12")
38
+ def _(event):
39
+ """When F12 is pressed, send 'proceed' as input immediately."""
40
+ buf = event.app.current_buffer
41
+ buf.text = "proceed"
42
+ buf.validate_and_handle()
43
+
44
+ style = Style.from_dict(
45
+ {
46
+ "bottom-toolbar": "bg:#333333 #ffffff",
47
+ "b": "bold",
48
+ "prompt": "bold bg:#000080 #ffffff",
49
+ }
50
+ )
43
51
 
44
52
  def get_toolbar():
45
- if mode['multiline']:
46
- return HTML('<b>Multiline mode (Esc+Enter to submit). Type /single to switch.</b>')
53
+ f12_hint = " Press <b>F12</b> to auto-fill 'proceed' and submit."
54
+ if mode["multiline"]:
55
+ return HTML(
56
+ f"<b>Multiline mode (Esc+Enter to submit). Type /single to switch.</b>{f12_hint}"
57
+ )
47
58
  else:
48
- return HTML('<b>Single-line mode (Enter to submit). Type /multi for multiline.</b>')
59
+ return HTML(
60
+ f"<b>Single-line mode (Enter to submit). Type /multi for multiline.</b>{f12_hint}"
61
+ )
49
62
 
50
63
  session = PromptSession(
51
64
  multiline=False,
52
65
  key_bindings=bindings,
53
66
  editing_mode=EditingMode.EMACS,
54
67
  bottom_toolbar=get_toolbar,
55
- style=style
68
+ style=style,
56
69
  )
57
70
 
58
- prompt_icon = HTML('<prompt>💬 </prompt>')
71
+ prompt_icon = HTML("<prompt>💬 </prompt>")
59
72
 
60
73
  while True:
61
74
  response = session.prompt(prompt_icon)
62
- if not mode['multiline'] and response.strip() == '/multi':
63
- mode['multiline'] = True
75
+ if not mode["multiline"] and response.strip() == "/multi":
76
+ mode["multiline"] = True
64
77
  session.multiline = True
65
78
  continue
66
- elif mode['multiline'] and response.strip() == '/single':
67
- mode['multiline'] = False
79
+ elif mode["multiline"] and response.strip() == "/single":
80
+ mode["multiline"] = False
68
81
  session.multiline = False
69
82
  continue
70
83
  else:
71
84
  return response
72
-
73
-
74
- from janito.agent.tool_registry import register_tool
@@ -1,31 +1,33 @@
1
1
  from janito.agent.tool_registry import register_tool
2
2
  from janito.agent.tools.utils import expand_path, display_path
3
- from janito.agent.tools.tool_base import ToolBase
3
+ from janito.agent.tool_base import ToolBase
4
+
4
5
 
5
6
  @register_tool(name="create_directory")
6
7
  class CreateDirectoryTool(ToolBase):
7
8
  """
8
9
  Create a new directory at the specified path.
9
- """
10
- def call(self, path: str, overwrite: bool = False) -> str:
11
- """
12
- Create a new directory at the specified path.
13
10
 
14
- Args:
15
- path (str): Path for the new directory.
16
- overwrite (bool, optional): Whether to overwrite if the directory exists. Defaults to False.
11
+ Args:
12
+ path (str): Path for the new directory.
13
+ overwrite (bool, optional): Whether to overwrite if the directory exists. Defaults to False.
14
+ Returns:
15
+ str: Status message indicating the result. Example:
16
+ - "\u2705 Successfully created the directory at ..."
17
+ - "\u2757 Cannot create directory: ..."
18
+ """
17
19
 
18
- Returns:
19
- str: Status message indicating the result. Example:
20
- - "✅ Successfully created the directory at ..."
21
- - "❗ Cannot create directory: ..."
22
- """
20
+ def call(self, path: str, overwrite: bool = False) -> str:
23
21
  original_path = path
24
22
  path = expand_path(path)
25
23
  disp_path = display_path(original_path, path)
24
+ import os
25
+
26
26
  if os.path.exists(path):
27
27
  if not os.path.isdir(path):
28
- self.report_error(f"❌ Path '{disp_path}' exists and is not a directory.")
29
- return f" Path '{disp_path}' exists and is not a directory."
28
+ self.report_error(
29
+ f"\u274c Path '{disp_path}' exists and is not a directory."
30
+ )
31
+ return f"\u274c Path '{disp_path}' exists and is not a directory."
30
32
  # Directory creation logic would go here
31
- return f" Successfully created the directory at '{disp_path}'."
33
+ return f"\u2705 Successfully created the directory at '{disp_path}'."
@@ -1,52 +1,47 @@
1
1
  import os
2
+ import shutil
2
3
  from janito.agent.tool_registry import register_tool
3
4
  from janito.agent.tools.utils import expand_path, display_path
4
- from janito.agent.tools.tool_base import ToolBase
5
+ from janito.agent.tool_base import ToolBase
5
6
  from janito.agent.tools.tools_utils import pluralize
6
7
 
8
+
7
9
  @register_tool(name="create_file")
8
10
  class CreateFileTool(ToolBase):
9
11
  """
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
- """
14
- Create or update a file with the given content.
12
+ Create a new file with the given content. Fails if the file already exists.
15
13
 
16
- Args:
17
- path (str): Path to the file to create or update.
18
- content (str): Content to write to the file.
19
- overwrite (bool, optional): Whether to overwrite if the file exists. Defaults to False.
14
+ This tool will NOT overwrite existing files. If the file already exists, the operation fails and no changes are made to the file itself.
15
+
16
+ Args:
17
+ path (str): Path to the file to create.
18
+ content (str): Content to write to the file.
19
+ backup (bool, optional): If True, create a backup (.bak) before returning an error if the file exists. Defaults to False.
20
+ Returns:
21
+ str: Status message indicating the result. Example:
22
+ - "\u2705 Successfully created the file at ..."
23
+ - "\u2757 Cannot create file: ..."
24
+ """
20
25
 
21
- Returns:
22
- str: Status message indicating the result. Example:
23
- - "✅ Successfully created the file at ..."
24
- - "❗ Cannot create file: ..."
25
- """
26
+ def call(self, path: str, content: str, backup: bool = False) -> str:
26
27
  original_path = path
27
28
  path = expand_path(path)
28
- updating = os.path.exists(path) and not os.path.isdir(path)
29
29
  disp_path = display_path(original_path, path)
30
30
  if os.path.exists(path):
31
31
  if os.path.isdir(path):
32
- self.report_error(" Error: is a directory")
33
- return f" Cannot create file: '{disp_path}' is an existing directory."
34
- if not overwrite:
35
- self.report_error(f"❗ Error: file '{disp_path}' exists and overwrite is False")
36
- return f" Cannot create file: '{disp_path}' already exists and overwrite is False."
37
- if updating and overwrite:
38
- self.report_info(f"📝 Updating file: '{disp_path}' ... ")
39
- else:
40
- self.report_info(f"📝 Creating file: '{disp_path}' ... ")
41
- old_lines = None
42
- if updating and overwrite:
43
- with open(path, "r", encoding="utf-8") as f:
44
- old_lines = sum(1 for _ in f)
45
- with open(path, "w", encoding="utf-8") as f:
32
+ self.report_error("\u274c Error: is a directory")
33
+ return f"\u274c Cannot create file: '{disp_path}' is an existing directory."
34
+ if backup:
35
+ shutil.copy2(path, path + ".bak")
36
+ self.report_error(f"\u2757 Error: file '{disp_path}' already exists")
37
+ return f"\u2757 Cannot create file: '{disp_path}' already exists."
38
+ # Ensure parent directories exist
39
+ dir_name = os.path.dirname(path)
40
+ if dir_name:
41
+ os.makedirs(dir_name, exist_ok=True)
42
+ self.report_info(f"\U0001f4dd Creating file: '{disp_path}' ... ")
43
+ with open(path, "w", encoding="utf-8", errors="replace") as f:
46
44
  f.write(content)
47
- new_lines = content.count('\n') + 1 if content else 0
48
- if old_lines is not None:
49
- self.report_success(f" {new_lines} {pluralize('line', new_lines)}")
50
- return f"✅ Successfully updated the file at '{disp_path}' ({old_lines} > {new_lines} lines)."
51
- self.report_success(f"✅ {new_lines} {pluralize('line', new_lines)}")
52
- return f"✅ Successfully created the file at '{disp_path}' ({new_lines} lines)."
45
+ new_lines = content.count("\n") + 1 if content else 0
46
+ self.report_success(f"\u2705 {new_lines} {pluralize('line', new_lines)}")
47
+ return f"\u2705 Successfully created the file at '{disp_path}' ({new_lines} lines)."
@@ -2,34 +2,39 @@ import requests
2
2
  from bs4 import BeautifulSoup
3
3
  from janito.agent.tool_registry import register_tool
4
4
 
5
- from janito.agent.tools.tool_base import ToolBase
5
+ from janito.agent.tool_base import ToolBase
6
+
6
7
 
7
8
  @register_tool(name="fetch_url")
8
9
  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
- """
12
- Fetch the content of a web page and extract its text.
10
+ """
11
+ Fetch the content of a web page and extract its text.
13
12
 
14
- Args:
15
- url (str): The URL of the web page to fetch.
16
- search_strings (list[str], optional): Strings to search for in the page content.
13
+ Args:
14
+ url (str): The URL of the web page to fetch.
15
+ search_strings (list[str], optional): Strings to search for in the page content.
16
+ Returns:
17
+ str: Extracted text content from the web page, or a warning message. Example:
18
+ - "<main text content...>"
19
+ - "No lines found for the provided search strings."
20
+ - "Warning: Empty URL provided. Operation skipped."
21
+ """
17
22
 
18
- Returns:
19
- str: Extracted text content from the web page, or a warning message. Example:
20
- - "<main text content...>"
21
- - "No lines found for the provided search strings."
22
- - "Warning: Empty URL provided. Operation skipped."
23
- """
23
+ def call(self, url: str, search_strings: list[str] = None) -> str:
24
24
  if not url.strip():
25
25
  self.report_warning("⚠️ Warning: Empty URL provided. Operation skipped.")
26
26
  return "Warning: Empty URL provided. Operation skipped."
27
27
  self.report_info(f"🌐 Fetching URL: {url} ... ")
28
28
  response = requests.get(url, timeout=10)
29
29
  response.raise_for_status()
30
- self.update_progress({'event': 'progress', 'message': f"Fetched URL with status {response.status_code}"})
31
- soup = BeautifulSoup(response.text, 'html.parser')
32
- text = soup.get_text(separator='\n')
30
+ self.update_progress(
31
+ {
32
+ "event": "progress",
33
+ "message": f"Fetched URL with status {response.status_code}",
34
+ }
35
+ )
36
+ soup = BeautifulSoup(response.text, "html.parser")
37
+ text = soup.get_text(separator="\n")
33
38
 
34
39
  if search_strings:
35
40
  filtered = []
@@ -41,10 +46,9 @@ class FetchUrlTool(ToolBase):
41
46
  snippet = text[start:end]
42
47
  filtered.append(snippet)
43
48
  if filtered:
44
- text = '\n...\n'.join(filtered)
49
+ text = "\n...\n".join(filtered)
45
50
  else:
46
51
  text = "No lines found for the provided search strings."
47
52
 
48
53
  self.report_success("✅ Result")
49
54
  return text
50
-