janito 2.2.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.
Files changed (57) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/setup_agent.py +14 -5
  3. janito/agent/templates/profiles/system_prompt_template_main.txt.j2 +3 -1
  4. janito/cli/chat_mode/bindings.py +6 -0
  5. janito/cli/chat_mode/session.py +16 -0
  6. janito/cli/chat_mode/shell/commands/__init__.py +3 -0
  7. janito/cli/chat_mode/shell/commands/exec.py +27 -0
  8. janito/cli/chat_mode/shell/commands/tools.py +17 -6
  9. janito/cli/chat_mode/shell/session/manager.py +1 -0
  10. janito/cli/chat_mode/toolbar.py +1 -0
  11. janito/cli/cli_commands/model_utils.py +95 -84
  12. janito/cli/config.py +2 -1
  13. janito/cli/core/getters.py +33 -31
  14. janito/cli/core/runner.py +165 -148
  15. janito/cli/core/setters.py +5 -1
  16. janito/cli/main_cli.py +12 -1
  17. janito/cli/prompt_core.py +5 -2
  18. janito/cli/rich_terminal_reporter.py +22 -3
  19. janito/cli/single_shot_mode/handler.py +11 -1
  20. janito/cli/verbose_output.py +1 -1
  21. janito/config_manager.py +112 -110
  22. janito/driver_events.py +14 -0
  23. janito/drivers/azure_openai/driver.py +38 -3
  24. janito/drivers/driver_registry.py +0 -2
  25. janito/drivers/openai/driver.py +196 -36
  26. janito/llm/auth.py +63 -62
  27. janito/llm/driver.py +7 -1
  28. janito/llm/driver_config.py +1 -0
  29. janito/provider_config.py +7 -3
  30. janito/provider_registry.py +18 -0
  31. janito/providers/__init__.py +1 -0
  32. janito/providers/anthropic/provider.py +4 -2
  33. janito/providers/azure_openai/model_info.py +16 -15
  34. janito/providers/azure_openai/provider.py +33 -2
  35. janito/providers/deepseek/provider.py +3 -0
  36. janito/providers/google/model_info.py +21 -29
  37. janito/providers/google/provider.py +52 -38
  38. janito/providers/mistralai/provider.py +5 -2
  39. janito/providers/openai/provider.py +4 -0
  40. janito/providers/provider_static_info.py +2 -3
  41. janito/tools/adapters/local/adapter.py +33 -11
  42. janito/tools/adapters/local/delete_text_in_file.py +4 -7
  43. janito/tools/adapters/local/move_file.py +3 -13
  44. janito/tools/adapters/local/remove_directory.py +6 -17
  45. janito/tools/adapters/local/remove_file.py +4 -10
  46. janito/tools/adapters/local/replace_text_in_file.py +6 -9
  47. janito/tools/adapters/local/search_text/match_lines.py +1 -1
  48. janito/tools/tools_adapter.py +78 -6
  49. janito/version.py +1 -1
  50. {janito-2.2.0.dist-info → janito-2.3.1.dist-info}/METADATA +149 -10
  51. {janito-2.2.0.dist-info → janito-2.3.1.dist-info}/RECORD +55 -56
  52. janito/drivers/google_genai/driver.py +0 -54
  53. janito/drivers/google_genai/schema_generator.py +0 -67
  54. {janito-2.2.0.dist-info → janito-2.3.1.dist-info}/WHEEL +0 -0
  55. {janito-2.2.0.dist-info → janito-2.3.1.dist-info}/entry_points.txt +0 -0
  56. {janito-2.2.0.dist-info → janito-2.3.1.dist-info}/licenses/LICENSE +0 -0
  57. {janito-2.2.0.dist-info → janito-2.3.1.dist-info}/top_level.txt +0 -0
@@ -3,20 +3,35 @@ from janito.tools.tools_adapter import ToolsAdapterBase as ToolsAdapter
3
3
 
4
4
 
5
5
  class LocalToolsAdapter(ToolsAdapter):
6
- def disable_execution_tools(self):
7
- """Unregister all tools with provides_execution = True."""
8
- to_remove = [name for name, entry in self._tools.items()
9
- if getattr(entry["instance"], "provides_execution", False)]
10
- for name in to_remove:
11
- self.unregister_tool(name)
6
+ def set_execution_tools_enabled(self, enabled: bool):
7
+ """
8
+ Dynamically include or exclude execution tools from the enabled_tools set.
9
+ If enabled_tools is None, all tools are enabled (default). If set, restricts enabled tools.
10
+ """
11
+ all_tool_names = set(self._tools.keys())
12
+ exec_tool_names = {
13
+ name for name, entry in self._tools.items()
14
+ if getattr(entry["instance"], "provides_execution", False)
15
+ }
16
+ if self._enabled_tools is None:
17
+ # If not restricted, create a new enabled-tools set excluding execution tools if disabling
18
+ if enabled:
19
+ self._enabled_tools = None # all tools enabled
20
+ else:
21
+ self._enabled_tools = all_tool_names - exec_tool_names
22
+ else:
23
+ if enabled:
24
+ self._enabled_tools |= exec_tool_names
25
+ else:
26
+ self._enabled_tools -= exec_tool_names
12
27
 
13
28
  """
14
29
  Adapter for local, statically registered tools in the agent/tools system.
15
30
  Handles registration, lookup, enabling/disabling, listing, and now, tool execution (merged from executor).
16
31
  """
17
32
 
18
- def __init__(self, tools=None, event_bus=None, allowed_tools=None, workdir=None):
19
- super().__init__(tools=tools, event_bus=event_bus, allowed_tools=allowed_tools)
33
+ def __init__(self, tools=None, event_bus=None, enabled_tools=None, workdir=None):
34
+ super().__init__(tools=tools, event_bus=event_bus, enabled_tools=enabled_tools)
20
35
  self._tools: Dict[str, Dict[str, Any]] = {}
21
36
  self.workdir = workdir
22
37
  if self.workdir:
@@ -56,13 +71,19 @@ class LocalToolsAdapter(ToolsAdapter):
56
71
  return self._tools[name]["instance"] if name in self._tools else None
57
72
 
58
73
  def list_tools(self):
59
- return list(self._tools.keys())
74
+ if self._enabled_tools is None:
75
+ return list(self._tools.keys())
76
+ return [name for name in self._tools.keys() if name in self._enabled_tools]
60
77
 
61
78
  def get_tool_classes(self):
62
- return [entry["class"] for entry in self._tools.values()]
79
+ if self._enabled_tools is None:
80
+ return [entry["class"] for entry in self._tools.values()]
81
+ return [entry["class"] for name, entry in self._tools.items() if name in self._enabled_tools]
63
82
 
64
83
  def get_tools(self):
65
- return [entry["instance"] for entry in self._tools.values()]
84
+ if self._enabled_tools is None:
85
+ return [entry["instance"] for entry in self._tools.values()]
86
+ return [entry["instance"] for name, entry in self._tools.items() if name in self._enabled_tools]
66
87
 
67
88
 
68
89
  def add_tool(self, tool):
@@ -94,3 +115,4 @@ def register_local_tool(tool=None):
94
115
  if tool is None:
95
116
  return decorator
96
117
  return decorator(tool)
118
+
@@ -15,7 +15,7 @@ class DeleteTextInFileTool(ToolBase):
15
15
  file_path (str): Path to the file to modify.
16
16
  start_marker (str): The starting delimiter string.
17
17
  end_marker (str): The ending delimiter string.
18
- backup (bool, optional): If True, create a backup (.bak) before deleting. Defaults to False.
18
+
19
19
  Returns:
20
20
  str: Status message indicating the result.
21
21
  """
@@ -52,9 +52,7 @@ class DeleteTextInFileTool(ToolBase):
52
52
  "No blocks found between markers in {file_path}.",
53
53
  file_path=file_path,
54
54
  )
55
- backup_path = file_path + ".bak"
56
- if backup:
57
- self._backup_file(file_path, backup_path)
55
+
58
56
  new_content, deleted_blocks = self._delete_blocks(
59
57
  content, start_marker, end_marker
60
58
  )
@@ -62,10 +60,9 @@ class DeleteTextInFileTool(ToolBase):
62
60
  validation_result = validate_file_syntax(file_path)
63
61
  self._report_success(match_lines)
64
62
  return tr(
65
- "Deleted {count} block(s) between markers in {file_path}. (backup at {backup_path})",
63
+ "Deleted {count} block(s) between markers in {file_path}. ",
66
64
  count=deleted_blocks,
67
- file_path=file_path,
68
- backup_path=backup_path if backup else "N/A",
65
+ file_path=file_path
69
66
  ) + (f"\n{validation_result}" if validation_result else "")
70
67
  except Exception as e:
71
68
  self.report_error(tr(" ❌ Error: {error}", error=e), ReportAction.REPLACE)
@@ -16,7 +16,7 @@ class MoveFileTool(ToolBase):
16
16
  src_path (str): Source file or directory path.
17
17
  dest_path (str): Destination file or directory path.
18
18
  overwrite (bool, optional): Whether to overwrite if the destination exists. Defaults to False.
19
- backup (bool, optional): If True, create a backup (.bak for files, .bak.zip for directories) of the destination before moving if it exists. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
19
+ backup (bool, optional): Deprecated. No backups are created anymore. This flag is ignored. Defaults to False.
20
20
  Returns:
21
21
  str: Status message indicating the result.
22
22
  """
@@ -60,11 +60,7 @@ class MoveFileTool(ToolBase):
60
60
  shutil.move(src, dest)
61
61
  self.report_success(tr("✅ Move complete."))
62
62
  msg = tr("✅ Move complete.")
63
- if backup_path:
64
- msg += tr(
65
- " (backup at {backup_disp})",
66
- backup_disp=display_path(backup_path),
67
- )
63
+
68
64
  return msg
69
65
  except Exception as e:
70
66
  self.report_error(tr("❌ Error moving: {error}", error=e))
@@ -116,13 +112,7 @@ class MoveFileTool(ToolBase):
116
112
  "❗ Destination '{disp_dest}' already exists and overwrite is False.",
117
113
  disp_dest=disp_dest,
118
114
  )
119
- if backup:
120
- if os.path.isfile(dest):
121
- backup_path = dest + ".bak"
122
- shutil.copy2(dest, backup_path)
123
- elif os.path.isdir(dest):
124
- backup_path = dest.rstrip("/\\") + ".bak.zip"
125
- shutil.make_archive(dest.rstrip("/\\") + ".bak", "zip", dest)
115
+
126
116
  try:
127
117
  if os.path.isfile(dest):
128
118
  os.remove(dest)
@@ -28,36 +28,25 @@ class RemoveDirectoryTool(ToolBase):
28
28
  disp_path = display_path(file_path)
29
29
  self.report_action(
30
30
  tr("🗃️ Remove directory '{disp_path}' ...", disp_path=disp_path),
31
- ReportAction.CREATE,
31
+ ReportAction.DELETE,
32
32
  )
33
- backup_zip = None
33
+
34
34
  try:
35
35
  if recursive:
36
- # Backup before recursive removal
37
- if os.path.exists(file_path) and os.path.isdir(file_path):
38
- backup_zip = file_path.rstrip("/\\") + ".bak.zip"
39
- with zipfile.ZipFile(backup_zip, "w", zipfile.ZIP_DEFLATED) as zipf:
40
- for root, dirs, files in os.walk(file_path):
41
- for file in files:
42
- abs_path = os.path.join(root, file)
43
- rel_path = os.path.relpath(
44
- abs_path, os.path.dirname(file_path)
45
- )
46
- zipf.write(abs_path, rel_path)
36
+
47
37
  shutil.rmtree(file_path)
48
38
  else:
49
39
  os.rmdir(file_path)
50
40
  self.report_success(
51
41
  tr("✅ 1 {dir_word}", dir_word=pluralize("directory", 1)),
52
- ReportAction.CREATE,
42
+ ReportAction.DELETE,
53
43
  )
54
44
  msg = tr("Directory removed: {disp_path}", disp_path=disp_path)
55
- if backup_zip:
56
- msg += tr(" (backup at {backup_zip})", backup_zip=backup_zip)
45
+
57
46
  return msg
58
47
  except Exception as e:
59
48
  self.report_error(
60
49
  tr(" ❌ Error removing directory: {error}", error=e),
61
- ReportAction.REMOVE,
50
+ ReportAction.DELETE,
62
51
  )
63
52
  return tr("Error removing directory: {error}", error=e)
@@ -15,7 +15,7 @@ class RemoveFileTool(ToolBase):
15
15
 
16
16
  Args:
17
17
  file_path (str): Path to the file to remove.
18
- backup (bool, optional): If True, create a backup (.bak) before removing. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
18
+ backup (bool, optional): Deprecated. Backups are no longer created. Flag ignored.
19
19
  Returns:
20
20
  str: Status message indicating the result. Example:
21
21
  - " Successfully removed the file at ..."
@@ -28,7 +28,7 @@ class RemoveFileTool(ToolBase):
28
28
  original_path = file_path
29
29
  path = file_path # Using file_path as is
30
30
  disp_path = display_path(original_path)
31
- backup_path = None
31
+
32
32
  # Report initial info about what is going to be removed
33
33
  self.report_action(
34
34
  tr("🗑️ Remove file '{disp_path}' ...", disp_path=disp_path),
@@ -41,20 +41,14 @@ class RemoveFileTool(ToolBase):
41
41
  self.report_error(tr("❌ Path is not a file."), ReportAction.DELETE)
42
42
  return tr("❌ Path is not a file.")
43
43
  try:
44
- if backup:
45
- backup_path = path + ".bak"
46
- shutil.copy2(path, backup_path)
44
+
47
45
  os.remove(path)
48
46
  self.report_success(tr("✅ File removed"), ReportAction.DELETE)
49
47
  msg = tr(
50
48
  "✅ Successfully removed the file at '{disp_path}'.",
51
49
  disp_path=disp_path,
52
50
  )
53
- if backup_path:
54
- msg += tr(
55
- " (backup at {backup_disp})",
56
- backup_disp=display_path(original_path + ".bak"),
57
- )
51
+
58
52
  return msg
59
53
  except Exception as e:
60
54
  self.report_error(
@@ -21,10 +21,10 @@ class ReplaceTextInFileTool(ToolBase):
21
21
  search_text (str): The exact text to search for (including indentation).
22
22
  replacement_text (str): The text to replace with (including indentation).
23
23
  replace_all (bool): If True, replace all occurrences; otherwise, only the first occurrence.
24
- backup (bool, optional): If True, create a backup (.bak) before replacing. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
24
+ backup (bool, optional): Deprecated. No backups are created anymore and this flag is ignored. Defaults to False.
25
25
  Returns:
26
26
  str: Status message. Example:
27
- - "Text replaced in /path/to/file (backup at /path/to/file.bak)"
27
+ - "Text replaced in /path/to/file"
28
28
  - "No changes made. [Warning: Search text not found in file] Please review the original file."
29
29
  - "Error replacing text: <error message>"
30
30
  """
@@ -63,9 +63,7 @@ class ReplaceTextInFileTool(ToolBase):
63
63
  content, search_text, replacement_text, replace_all, occurrences
64
64
  )
65
65
  file_changed = new_content != content
66
- backup_path = file_path + ".bak"
67
- if backup and file_changed:
68
- self._backup_file(file_path, backup_path)
66
+ backup_path = None
69
67
  validation_result = ""
70
68
  if file_changed:
71
69
  self._write_file_content(file_path, new_content)
@@ -88,7 +86,7 @@ class ReplaceTextInFileTool(ToolBase):
88
86
  replace_all,
89
87
  )
90
88
  return self._format_final_msg(
91
- file_path, warning, backup_path, match_info, details
89
+ file_path, warning, match_info, details
92
90
  ) + (f"\n{validation_result}" if validation_result else "")
93
91
  except Exception as e:
94
92
  self.report_error(tr(" ❌ Error"), ReportAction.REPLACE)
@@ -260,13 +258,12 @@ class ReplaceTextInFileTool(ToolBase):
260
258
  details = ""
261
259
  return match_info, details
262
260
 
263
- def _format_final_msg(self, file_path, warning, backup_path, match_info, details):
261
+ def _format_final_msg(self, file_path, warning, match_info, details):
264
262
  """Format the final status message."""
265
263
  return tr(
266
- "Text replaced in {file_path}{warning} (backup at {backup_path}). {match_info}{details}",
264
+ "Text replaced in {file_path}{warning}. {match_info}{details}",
267
265
  file_path=file_path,
268
266
  warning=warning,
269
- backup_path=backup_path,
270
267
  match_info=match_info,
271
268
  details=details,
272
269
  )
@@ -24,7 +24,7 @@ def match_line(line, pattern, regex, use_regex, case_sensitive):
24
24
  if use_regex:
25
25
  return regex and regex.search(line)
26
26
  if not case_sensitive:
27
- return pattern in line.lower()
27
+ return pattern.lower() in line.lower()
28
28
  return pattern in line
29
29
 
30
30
 
@@ -13,11 +13,11 @@ class ToolsAdapterBase:
13
13
  """
14
14
 
15
15
  def __init__(
16
- self, tools=None, event_bus=None, allowed_tools: Optional[list] = None
16
+ self, tools=None, event_bus=None, enabled_tools: Optional[list] = None
17
17
  ):
18
18
  self._tools = tools or []
19
19
  self._event_bus = event_bus # event bus can be set on all adapters
20
- self._allowed_tools = set(allowed_tools) if allowed_tools is not None else None
20
+ self._enabled_tools = set(enabled_tools) if enabled_tools is not None else None
21
21
  self.verbose_tools = False
22
22
 
23
23
  def set_verbose_tools(self, value: bool):
@@ -32,8 +32,10 @@ class ToolsAdapterBase:
32
32
  self._event_bus = bus
33
33
 
34
34
  def get_tools(self):
35
- """Return the list of tools managed by this provider."""
36
- return self._tools
35
+ """Return the list of enabled tools managed by this provider."""
36
+ if self._enabled_tools is None:
37
+ return self._tools
38
+ return [tool for tool in self._tools if getattr(tool, 'tool_name', None) in self._enabled_tools]
37
39
 
38
40
  def add_tool(self, tool):
39
41
  self._tools.append(tool)
@@ -84,12 +86,82 @@ class ToolsAdapterBase:
84
86
 
85
87
  return result
86
88
 
89
+ def _get_tool_callable(self, tool):
90
+ """Helper to retrieve the primary callable of a tool instance."""
91
+ if callable(tool):
92
+ return tool
93
+ if hasattr(tool, "execute") and callable(getattr(tool, "execute")):
94
+ return getattr(tool, "execute")
95
+ if hasattr(tool, "run") and callable(getattr(tool, "run")):
96
+ return getattr(tool, "run")
97
+ raise ValueError("Provided tool is not executable.")
98
+
99
+ def _validate_arguments_against_signature(self, func, arguments: dict):
100
+ """Validate provided arguments against a callable signature.
101
+
102
+ Returns an error string if validation fails, otherwise ``None``.
103
+ """
104
+ import inspect
105
+
106
+ if arguments is None:
107
+ arguments = {}
108
+ # Ensure the input is a dict to avoid breaking the inspect-based logic
109
+ if not isinstance(arguments, dict):
110
+ return "Tool arguments should be provided as an object / mapping"
111
+
112
+ sig = inspect.signature(func)
113
+ params = sig.parameters
114
+
115
+ # Check for unexpected arguments (unless **kwargs is accepted)
116
+ accepts_kwargs = any(
117
+ p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()
118
+ )
119
+ if not accepts_kwargs:
120
+ unexpected = [k for k in arguments.keys() if k not in params]
121
+ if unexpected:
122
+ return (
123
+ "Unexpected argument(s): " + ", ".join(sorted(unexpected))
124
+ )
125
+
126
+ # Check for missing required arguments (ignoring *args / **kwargs / self)
127
+ required_params = [
128
+ name
129
+ for name, p in params.items()
130
+ if p.kind in (
131
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
132
+ inspect.Parameter.KEYWORD_ONLY,
133
+ )
134
+ and p.default is inspect._empty
135
+ and name != "self"
136
+ ]
137
+ missing = [name for name in required_params if name not in arguments]
138
+ if missing:
139
+ return "Missing required argument(s): " + ", ".join(sorted(missing))
140
+
141
+ return None
142
+
87
143
  def execute_by_name(
88
144
  self, tool_name: str, *args, request_id=None, arguments=None, **kwargs
89
145
  ):
90
146
  self._check_tool_permissions(tool_name, request_id, arguments)
91
147
  tool = self.get_tool(tool_name)
92
148
  self._ensure_tool_exists(tool, tool_name, request_id, arguments)
149
+ func = self._get_tool_callable(tool)
150
+ # First, validate arguments against the callable signature to catch unexpected / missing params
151
+ sig_error = self._validate_arguments_against_signature(func, arguments)
152
+ if sig_error:
153
+ if self._event_bus:
154
+ self._event_bus.publish(
155
+ ToolCallError(
156
+ tool_name=tool_name,
157
+ request_id=request_id,
158
+ error=sig_error,
159
+ arguments=arguments,
160
+ )
161
+ )
162
+ return sig_error
163
+
164
+ # Optionally validate against JSON schema if available
93
165
  schema = getattr(tool, "schema", None)
94
166
  if schema and arguments is not None:
95
167
  validation_error = self._validate_arguments_against_schema(
@@ -159,8 +231,8 @@ class ToolsAdapterBase:
159
231
  )
160
232
 
161
233
  def _check_tool_permissions(self, tool_name, request_id, arguments):
162
- if self._allowed_tools is not None and tool_name not in self._allowed_tools:
163
- error_msg = f"Tool '{tool_name}' is not permitted by adapter allow-list."
234
+ if self._enabled_tools is not None and tool_name not in self._enabled_tools:
235
+ error_msg = f"Tool '{tool_name}' is not enabled in this adapter."
164
236
  if self._event_bus:
165
237
  self._event_bus.publish(
166
238
  ToolCallError(
janito/version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # janito/version.py
2
2
  """Single source of truth for the janito package version."""
3
3
 
4
- __version__ = "2.2.0"
4
+ __version__ = "2.3.1"