janito 1.2.0__py3-none-any.whl → 1.3.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 (48) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/agent.py +27 -10
  3. janito/agent/config.py +37 -9
  4. janito/agent/config_utils.py +9 -0
  5. janito/agent/conversation.py +19 -3
  6. janito/agent/runtime_config.py +1 -0
  7. janito/agent/tool_handler.py +154 -52
  8. janito/agent/tools/__init__.py +9 -8
  9. janito/agent/tools/ask_user.py +2 -1
  10. janito/agent/tools/fetch_url.py +27 -35
  11. janito/agent/tools/file_ops.py +72 -0
  12. janito/agent/tools/find_files.py +47 -26
  13. janito/agent/tools/get_lines.py +58 -0
  14. janito/agent/tools/py_compile.py +26 -0
  15. janito/agent/tools/python_exec.py +47 -0
  16. janito/agent/tools/remove_directory.py +38 -0
  17. janito/agent/tools/replace_text_in_file.py +67 -0
  18. janito/agent/tools/run_bash_command.py +134 -0
  19. janito/agent/tools/search_files.py +52 -0
  20. janito/cli/_print_config.py +12 -12
  21. janito/cli/arg_parser.py +6 -1
  22. janito/cli/config_commands.py +56 -8
  23. janito/cli/runner.py +21 -9
  24. janito/cli_chat_shell/chat_loop.py +5 -3
  25. janito/cli_chat_shell/commands.py +34 -37
  26. janito/cli_chat_shell/config_shell.py +1 -1
  27. janito/cli_chat_shell/load_prompt.py +1 -1
  28. janito/cli_chat_shell/session_manager.py +11 -15
  29. janito/cli_chat_shell/ui.py +17 -8
  30. janito/render_prompt.py +3 -1
  31. janito/web/app.py +76 -19
  32. janito-1.3.0.dist-info/METADATA +142 -0
  33. janito-1.3.0.dist-info/RECORD +51 -0
  34. janito/agent/tools/bash_exec.py +0 -58
  35. janito/agent/tools/create_directory.py +0 -19
  36. janito/agent/tools/create_file.py +0 -43
  37. janito/agent/tools/file_str_replace.py +0 -48
  38. janito/agent/tools/move_file.py +0 -37
  39. janito/agent/tools/remove_file.py +0 -19
  40. janito/agent/tools/search_text.py +0 -41
  41. janito/agent/tools/view_file.py +0 -34
  42. janito/templates/system_instructions.j2 +0 -38
  43. janito-1.2.0.dist-info/METADATA +0 -85
  44. janito-1.2.0.dist-info/RECORD +0 -51
  45. {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/WHEEL +0 -0
  46. {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/entry_points.txt +0 -0
  47. {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/licenses/LICENSE +0 -0
  48. {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/top_level.txt +0 -0
janito/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.2.0"
1
+ __version__ = "1.3.0"
janito/agent/agent.py CHANGED
@@ -7,7 +7,7 @@ from janito.agent.conversation import ConversationHandler
7
7
  from janito.agent.tool_handler import ToolHandler
8
8
 
9
9
  class Agent:
10
- """LLM Agent capable of handling conversations and tool calls."""
10
+ """Agent capable of handling conversations and tool calls."""
11
11
 
12
12
  REFERER = "www.janito.dev"
13
13
  TITLE = "Janito"
@@ -19,10 +19,22 @@ class Agent:
19
19
  system_prompt: str | None = None,
20
20
  verbose_tools: bool = False,
21
21
  tool_handler = None,
22
- base_url: str = "https://openrouter.ai/api/v1"
22
+ base_url: str = "https://openrouter.ai/api/v1",
23
+ azure_openai_api_version: str = "2023-05-15",
24
+ use_azure_openai: bool = False
23
25
  ):
24
26
  """
25
- Initialize the Agent.
27
+ Initialize Agent,
28
+
29
+ Args:
30
+ api_key: API key for OpenAI-compatible service.
31
+ model: Model name to use.
32
+ system_prompt: Optional system prompt override.
33
+ verbose_tools: Enable verbose tool call logging.
34
+ tool_handler: Optional custom ToolHandler instance.
35
+ base_url: API base URL.
36
+ azure_openai_api_version: Azure OpenAI API version (default: "2023-05-15").
37
+ use_azure_openai: Whether to use Azure OpenAI client (default: False).
26
38
 
27
39
  Args:
28
40
  api_key: API key for OpenAI-compatible service.
@@ -35,12 +47,12 @@ class Agent:
35
47
  self.api_key = api_key
36
48
  self.model = model
37
49
  self.system_prompt = system_prompt
38
- if os.environ.get("USE_AZURE_OPENAI"):
50
+ if use_azure_openai:
39
51
  from openai import AzureOpenAI
40
52
  self.client = AzureOpenAI(
41
53
  api_key=api_key,
42
- azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
43
- api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2023-05-15"),
54
+ azure_endpoint=base_url,
55
+ api_version=azure_openai_api_version,
44
56
  )
45
57
  else:
46
58
  self.client = OpenAI(
@@ -60,7 +72,11 @@ class Agent:
60
72
  self.client, self.model, self.tool_handler
61
73
  )
62
74
 
63
- def chat(self, messages, on_content=None, on_tool_progress=None, verbose_response=False, spinner=False, max_tokens=None):
75
+ @property
76
+ def usage_history(self):
77
+ return self.conversation_handler.usage_history
78
+
79
+ def chat(self, messages, on_content=None, on_tool_progress=None, verbose_response=False, spinner=False, max_tokens=None, max_rounds=50):
64
80
  import time
65
81
  from janito.agent.conversation import ProviderError
66
82
 
@@ -69,11 +85,12 @@ class Agent:
69
85
  try:
70
86
  return self.conversation_handler.handle_conversation(
71
87
  messages,
72
- max_tokens=max_tokens,
88
+ max_rounds=max_rounds,
73
89
  on_content=on_content,
74
90
  on_tool_progress=on_tool_progress,
75
91
  verbose_response=verbose_response,
76
- spinner=spinner
92
+ spinner=spinner,
93
+ max_tokens=max_tokens
77
94
  )
78
95
  except ProviderError as e:
79
96
  error_data = getattr(e, 'error_data', {}) or {}
@@ -92,5 +109,5 @@ class Agent:
92
109
  else:
93
110
  print("Max retries reached. Raising error.")
94
111
  raise
95
- except Exception:
112
+
96
113
  raise
janito/agent/config.py CHANGED
@@ -40,14 +40,12 @@ class FileConfig(BaseConfig):
40
40
 
41
41
  def load(self):
42
42
  if self.path.exists():
43
- try:
44
- with open(self.path, 'r') as f:
45
- self._data = json.load(f)
46
- # Remove keys with value None (null in JSON)
47
- self._data = {k: v for k, v in self._data.items() if v is not None}
48
- except Exception as e:
49
- print(f"Warning: Failed to load config file {self.path}: {e}")
50
- self._data = {}
43
+ with open(self.path, 'r') as f:
44
+ self._data = json.load(f)
45
+ # Remove keys with value None (null in JSON)
46
+ self._data = {k: v for k, v in self._data.items() if v is not None}
47
+
48
+
51
49
  else:
52
50
  self._data = {}
53
51
 
@@ -57,6 +55,7 @@ class FileConfig(BaseConfig):
57
55
  json.dump(self._data, f, indent=2)
58
56
 
59
57
 
58
+
60
59
  CONFIG_OPTIONS = {
61
60
  "api_key": "API key for OpenAI-compatible service (required)",
62
61
  "model": "Model name to use (e.g., 'openai/gpt-4.1')",
@@ -64,9 +63,38 @@ CONFIG_OPTIONS = {
64
63
  "role": "Role description for the system prompt (e.g., 'software engineer')",
65
64
  "system_prompt": "Override the entire system prompt text",
66
65
  "temperature": "Sampling temperature (float, e.g., 0.0 - 2.0)",
67
- "max_tokens": "Maximum tokens for model response (int)"
66
+ "max_tokens": "Maximum tokens for model response (int)",
67
+ # Accept template.* keys as valid config keys (for CLI validation, etc.)
68
+ "template": "Template context dictionary for prompt rendering (nested)",
69
+ # Note: template.* keys are validated dynamically, not statically here
68
70
  }
69
71
 
72
+ class BaseConfig:
73
+ def __init__(self):
74
+ self._data = {}
75
+
76
+ def get(self, key, default=None):
77
+ return self._data.get(key, default)
78
+
79
+ def set(self, key, value):
80
+ self._data[key] = value
81
+
82
+ def all(self):
83
+ return self._data
84
+
85
+
86
+ """
87
+ Returns a dictionary suitable for passing as Jinja2 template variables.
88
+ Merges the nested 'template' dict (if present) and all flat 'template.*' keys.
89
+ Flat keys override nested dict keys if there is a conflict.
90
+ """
91
+ template_vars = dict(self._data.get("template", {}))
92
+ for k, v in self._data.items():
93
+ if k.startswith("template.") and k != "template":
94
+ template_vars[k[9:]] = v
95
+ return template_vars
96
+
97
+
70
98
  # Import defaults for reference
71
99
  from .config_defaults import CONFIG_DEFAULTS
72
100
 
@@ -0,0 +1,9 @@
1
+ def merge_configs(*configs):
2
+ """
3
+ Merge multiple config-like objects (with .all()) into one dict.
4
+ Later configs override earlier ones.
5
+ """
6
+ merged = {}
7
+ for cfg in configs:
8
+ merged.update(cfg.all())
9
+ return merged
@@ -16,8 +16,12 @@ class ConversationHandler:
16
16
  self.client = client
17
17
  self.model = model
18
18
  self.tool_handler = tool_handler
19
+ self.usage_history = []
19
20
 
20
21
  def handle_conversation(self, messages, max_rounds=50, on_content=None, on_tool_progress=None, verbose_response=False, spinner=False, max_tokens=None):
22
+ from janito.agent.runtime_config import runtime_config
23
+ max_tools = runtime_config.get('max_tools', None)
24
+ tool_calls_made = 0
21
25
  if not messages:
22
26
  raise ValueError("No prompt provided in messages")
23
27
 
@@ -95,16 +99,28 @@ class ConversationHandler:
95
99
 
96
100
  # If no tool calls, return the assistant's message and usage info
97
101
  if not choice.message.tool_calls:
102
+ # Store usage info in usage_history, linked to the next assistant message index
103
+ assistant_idx = len([m for m in messages if m.get('role') == 'assistant'])
104
+ self.usage_history.append({"assistant_index": assistant_idx, "usage": usage_info})
98
105
  return {
99
- "content": choice.message.content,
100
- "usage": usage_info
101
- }
106
+ "content": choice.message.content,
107
+ "usage": usage_info,
108
+ "usage_history": self.usage_history
109
+ }
102
110
 
111
+ from janito.agent.runtime_config import runtime_config
103
112
  tool_responses = []
113
+ # Sequential tool execution (default, only mode)
104
114
  for tool_call in choice.message.tool_calls:
115
+ if max_tools is not None and tool_calls_made >= max_tools:
116
+ raise MaxRoundsExceededError(f"Maximum number of tool calls ({max_tools}) reached in this chat session.")
105
117
  result = self.tool_handler.handle_tool_call(tool_call, on_progress=on_tool_progress)
106
118
  tool_responses.append({"tool_call_id": tool_call.id, "content": result})
119
+ tool_calls_made += 1
107
120
 
121
+ # Store usage info in usage_history, linked to the next assistant message index
122
+ assistant_idx = len([m for m in messages if m.get('role') == 'assistant'])
123
+ self.usage_history.append({"assistant_index": assistant_idx, "usage": usage_info})
108
124
  messages.append({"role": "assistant", "content": choice.message.content, "tool_calls": [tc.to_dict() for tc in choice.message.tool_calls]})
109
125
 
110
126
  for tr in tool_responses:
@@ -16,6 +16,7 @@ class UnifiedConfig:
16
16
  self.runtime_cfg = runtime_cfg
17
17
  self.effective_cfg = effective_cfg
18
18
 
19
+
19
20
  def get(self, key, default=None):
20
21
  val = self.runtime_cfg.get(key)
21
22
  if val is not None:
@@ -8,6 +8,8 @@ class ToolHandler:
8
8
  @classmethod
9
9
  def register_tool(cls, func):
10
10
  import inspect
11
+ import typing
12
+ from typing import get_origin, get_args
11
13
 
12
14
  name = func.__name__
13
15
  description = func.__doc__ or ""
@@ -23,18 +25,126 @@ class ToolHandler:
23
25
  if param.annotation is param.empty:
24
26
  raise TypeError(f"Parameter '{param_name}' in tool '{name}' is missing a type hint.")
25
27
  param_type = param.annotation
26
- json_type = "string"
27
- if param_type == int:
28
- json_type = "integer"
29
- elif param_type == float:
30
- json_type = "number"
31
- elif param_type == bool:
32
- json_type = "boolean"
33
- elif param_type == dict:
34
- json_type = "object"
35
- elif param_type == list:
36
- json_type = "array"
37
- params_schema["properties"][param_name] = {"type": json_type}
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 = {}
91
+
92
+ @classmethod
93
+ def register_tool(cls, func):
94
+ import inspect
95
+ import typing
96
+ from typing import get_origin, get_args
97
+
98
+ name = func.__name__
99
+ description = func.__doc__ or ""
100
+
101
+ sig = inspect.signature(func)
102
+ params_schema = {
103
+ "type": "object",
104
+ "properties": {},
105
+ "required": []
106
+ }
107
+
108
+ for param_name, param in sig.parameters.items():
109
+ if param.annotation is param.empty:
110
+ raise TypeError(f"Parameter '{param_name}' in tool '{name}' is missing a type hint.")
111
+ param_type = param.annotation
112
+ schema = {}
113
+
114
+ # Handle typing.Optional, typing.List, typing.Literal, etc.
115
+ origin = get_origin(param_type)
116
+ args = get_args(param_type)
117
+
118
+ if origin is typing.Union and type(None) in args:
119
+ # Optional[...] type
120
+ main_type = args[0] if args[1] is type(None) else args[1]
121
+ origin = get_origin(main_type)
122
+ args = get_args(main_type)
123
+ param_type = main_type
124
+ else:
125
+ main_type = param_type
126
+
127
+ if origin is list or origin is typing.List:
128
+ item_type = args[0] if args else str
129
+ item_schema = {"type": _pytype_to_json_type(item_type)}
130
+ schema = {"type": "array", "items": item_schema}
131
+ elif origin is typing.Literal:
132
+ schema = {"type": _pytype_to_json_type(type(args[0])), "enum": list(args)}
133
+ elif main_type == int:
134
+ schema = {"type": "integer"}
135
+ elif main_type == float:
136
+ schema = {"type": "number"}
137
+ elif main_type == bool:
138
+ schema = {"type": "boolean"}
139
+ elif main_type == dict:
140
+ schema = {"type": "object"}
141
+ elif main_type == list:
142
+ schema = {"type": "array", "items": {"type": "string"}}
143
+ else:
144
+ schema = {"type": "string"}
145
+
146
+ # Optionally add description if available in docstring (not implemented here)
147
+ params_schema["properties"][param_name] = schema
38
148
  if param.default is param.empty:
39
149
  params_schema["required"].append(param_name)
40
150
 
@@ -82,46 +192,38 @@ class ToolHandler:
82
192
  args = json.loads(tool_call.function.arguments)
83
193
  if self.verbose:
84
194
  print(f"[Tool Call] {tool_call.function.name} called with arguments: {args}")
195
+ import inspect
196
+ sig = inspect.signature(func)
197
+ if on_progress:
198
+ on_progress({
199
+ 'event': 'start',
200
+ 'call_id': call_id,
201
+ 'tool': tool_call.function.name,
202
+ 'args': args
203
+ })
204
+ if 'on_progress' in sig.parameters and on_progress is not None:
205
+ args['on_progress'] = on_progress
85
206
  try:
86
- import inspect
87
- sig = inspect.signature(func)
88
- if on_progress:
89
- on_progress({
90
- 'event': 'start',
91
- 'call_id': call_id,
92
- 'tool': tool_call.function.name,
93
- 'args': args
94
- })
95
- if 'on_progress' in sig.parameters and on_progress is not None:
96
- args['on_progress'] = on_progress
97
207
  result = func(**args)
98
- if self.verbose:
99
- preview = result
100
- if isinstance(result, str):
101
- lines = result.splitlines()
102
- if len(lines) > 10:
103
- preview = "\n".join(lines[:10]) + "\n... (truncated)"
104
- elif len(result) > 500:
105
- preview = result[:500] + "... (truncated)"
106
- print(f"[Tool Result] {tool_call.function.name} returned:\n{preview}")
107
- if on_progress:
108
- on_progress({
109
- 'event': 'finish',
110
- 'call_id': call_id,
111
- 'tool': tool_call.function.name,
112
- 'args': args,
113
- 'result': result
114
- })
115
- return result
116
208
  except Exception as e:
117
- tb = traceback.format_exc()
118
- if on_progress:
119
- on_progress({
120
- 'event': 'finish',
121
- 'call_id': call_id,
122
- 'tool': tool_call.function.name,
123
- 'args': args,
124
- 'error': str(e),
125
- 'traceback': tb
126
- })
127
- return f"Error running tool {tool_call.function.name}: {e}"
209
+ import traceback
210
+ error_message = f"[Tool Error] {type(e).__name__}: {e}\n" + traceback.format_exc()
211
+ result = error_message
212
+ if self.verbose:
213
+ preview = result
214
+ if isinstance(result, str):
215
+ lines = result.splitlines()
216
+ if len(lines) > 10:
217
+ preview = "\n".join(lines[:10]) + "\n... (truncated)"
218
+ elif len(result) > 500:
219
+ preview = result[:500] + "... (truncated)"
220
+ print(f"[Tool Result] {tool_call.function.name} returned:\n{preview}")
221
+ if on_progress:
222
+ on_progress({
223
+ 'event': 'finish',
224
+ 'call_id': call_id,
225
+ 'tool': tool_call.function.name,
226
+ 'args': args,
227
+ 'result': result
228
+ })
229
+ return result
@@ -1,11 +1,12 @@
1
1
  from .ask_user import ask_user
2
- from .create_directory import create_directory
3
- from .create_file import create_file
4
- from .remove_file import remove_file
5
- from .view_file import view_file
2
+ from .file_ops import create_file, create_directory, remove_file, move_file
3
+ from .get_lines import get_lines
4
+ from .replace_text_in_file import replace_text_in_file
6
5
  from .find_files import find_files
7
- from .search_text import search_text
8
- from .bash_exec import bash_exec
6
+ from .run_bash_command import run_bash_command
9
7
  from .fetch_url import fetch_url
10
- from .move_file import move_file
11
- from .file_str_replace import file_str_replace
8
+ from .python_exec import python_exec
9
+ from .py_compile import py_compile_file
10
+ from .search_files import search_files
11
+ from .remove_directory import remove_directory
12
+
@@ -11,7 +11,8 @@ def ask_user(question: str) -> str:
11
11
  """
12
12
  Ask the user a question and return their response.
13
13
 
14
- question: The question to ask the user
14
+ Args:
15
+ question (str): The question to ask the user.
15
16
  """
16
17
  from rich import print as rich_print
17
18
  from rich.panel import Panel
@@ -1,48 +1,40 @@
1
1
  import requests
2
+ from typing import Optional, Callable
2
3
  from bs4 import BeautifulSoup
3
4
  from janito.agent.tool_handler import ToolHandler
4
5
  from janito.agent.tools.rich_utils import print_info, print_success, print_error
5
6
 
6
7
  @ToolHandler.register_tool
7
- def fetch_url(url: str, search_strings: list[str] = None, on_progress: callable = None) -> str:
8
+ def fetch_url(url: str, search_strings: list[str] = None, on_progress: Optional[Callable[[dict], None]] = None) -> str:
8
9
  """
9
10
  Fetch the content of a web page and extract its text.
10
11
 
11
- url: The URL to fetch.
12
- search_strings: Optional list of strings to filter the extracted text around those strings.
13
- on_progress: Optional callback function for streaming progress updates.
12
+ Args:
13
+ url (str): The URL to fetch.
14
+ search_strings (list[str], optional): List of strings to filter the extracted text around those strings.
15
+ on_progress (callable, optional): Callback function for streaming progress updates.
14
16
  """
15
- if on_progress:
16
- on_progress({'event': 'start', 'url': url})
17
17
  print_info(f"\U0001F310 Fetching URL: {url} ... ")
18
- try:
19
- response = requests.get(url, timeout=10)
20
- response.raise_for_status()
21
- if on_progress:
22
- on_progress({'event': 'fetched', 'status_code': response.status_code})
23
- soup = BeautifulSoup(response.text, 'html.parser')
24
- text = soup.get_text(separator=' ', strip=True)
18
+ response = requests.get(url, timeout=10)
19
+ response.raise_for_status()
20
+ if on_progress:
21
+ on_progress({'event': 'fetched', 'status_code': response.status_code})
22
+ soup = BeautifulSoup(response.text, 'html.parser')
23
+ text = soup.get_text(separator='\n')
25
24
 
26
- if search_strings:
27
- filtered = []
28
- for s in search_strings:
29
- idx = text.find(s)
30
- if idx != -1:
31
- start = max(0, idx - 200)
32
- end = min(len(text), idx + len(s) + 200)
33
- snippet = text[start:end]
34
- filtered.append(snippet)
35
- if filtered:
36
- text = '\n...\n'.join(filtered)
37
- else:
38
- text = "No matches found for the provided search strings."
25
+ if search_strings:
26
+ filtered = []
27
+ for s in search_strings:
28
+ idx = text.find(s)
29
+ if idx != -1:
30
+ start = max(0, idx - 200)
31
+ end = min(len(text), idx + len(s) + 200)
32
+ snippet = text[start:end]
33
+ filtered.append(snippet)
34
+ if filtered:
35
+ text = '\n...\n'.join(filtered)
36
+ else:
37
+ text = "No matches found for the provided search strings."
39
38
 
40
- print_success("\u2705 Success")
41
- if on_progress:
42
- on_progress({'event': 'done'})
43
- return text
44
- except Exception as e:
45
- print_error(f"\u274c Error: {e}")
46
- if on_progress:
47
- on_progress({'event': 'error', 'error': str(e)})
48
- return f"\u274c Failed to fetch URL '{url}': {e}"
39
+ print_success("\u2705 Success")
40
+ return text
@@ -0,0 +1,72 @@
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, format_path
5
+
6
+ @ToolHandler.register_tool
7
+ def create_file(path: str, content: str, overwrite: bool = False) -> str:
8
+ """
9
+ Create a new file or update an existing file with the given content.
10
+
11
+ Args:
12
+ path (str): Path to the file to create or update.
13
+ content (str): Content to write to the file.
14
+ overwrite (bool): Whether to overwrite the file if it exists.
15
+ """
16
+ updating = os.path.exists(path) and not os.path.isdir(path)
17
+ if os.path.exists(path):
18
+ if os.path.isdir(path):
19
+ print_error("❌ Error: is a directory")
20
+ return f"❌ Cannot create file: '{path}' is an existing directory."
21
+ if not overwrite:
22
+ print_error(f"❗ Error: file '{path}' exists and overwrite is False")
23
+ return f"❗ Cannot create file: '{path}' already exists and overwrite is False."
24
+ if updating and overwrite:
25
+ print_info(f"📝 Updating file: '{format_path(path)}' ... ")
26
+ else:
27
+ print_info(f"📝 Creating file: '{format_path(path)}' ... ")
28
+ old_lines = None
29
+ if updating and overwrite:
30
+ with open(path, "r", encoding="utf-8") as f:
31
+ old_lines = sum(1 for _ in f)
32
+ with open(path, "w", encoding="utf-8") as f:
33
+ f.write(content)
34
+ print_success("✅ Success")
35
+ if old_lines is not None:
36
+ new_lines = content.count('\n') + 1 if content else 0
37
+ return f"✅ Successfully updated the file at '{path}' ({old_lines} > {new_lines} lines)."
38
+ else:
39
+ return f"✅ Successfully created the file at '{path}'."
40
+
41
+
42
+ @ToolHandler.register_tool
43
+ def remove_file(path: str) -> str:
44
+ print_info(f"🗑️ Removing file: '{format_path(path)}' ... ")
45
+ os.remove(path)
46
+ print_success("✅ Success")
47
+ return f"✅ Successfully deleted the file at '{path}'."
48
+
49
+ @ToolHandler.register_tool
50
+ def move_file(source_path: str, destination_path: str, overwrite: bool = False) -> str:
51
+ print_info(f"🚚 Moving '{format_path(source_path)}' to '{format_path(destination_path)}' ... ")
52
+ if not os.path.exists(source_path):
53
+ print_error("❌ Error: source does not exist")
54
+ return f"❌ Source path '{source_path}' does not exist."
55
+ if os.path.exists(destination_path):
56
+ if not overwrite:
57
+ print_error("❌ Error: destination exists and overwrite is False")
58
+ return f"❌ Destination path '{destination_path}' already exists. Use overwrite=True to replace it."
59
+ if os.path.isdir(destination_path):
60
+ shutil.rmtree(destination_path)
61
+ else:
62
+ os.remove(destination_path)
63
+ shutil.move(source_path, destination_path)
64
+ print_success("✅ Success")
65
+ return f"✅ Successfully moved '{source_path}' to '{destination_path}'."
66
+
67
+ @ToolHandler.register_tool
68
+ def create_directory(path: str) -> str:
69
+ print_info(f"📁 Creating directory: '{format_path(path)}' ... ")
70
+ os.makedirs(path, exist_ok=True)
71
+ print_success("✅ Success")
72
+ return f"✅ Directory '{path}' created successfully."