janito 3.4.0__py3-none-any.whl → 3.5.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 (162) hide show
  1. janito/README.md +3 -0
  2. janito/cli/chat_mode/bindings.py +50 -0
  3. janito/cli/chat_mode/session.py +12 -1
  4. janito/cli/chat_mode/shell/commands/multi.py +5 -0
  5. janito/cli/chat_mode/shell/commands/security/allowed_sites.py +47 -33
  6. janito/cli/cli_commands/check_tools.py +212 -0
  7. janito/cli/cli_commands/list_plugins.py +52 -43
  8. janito/cli/core/getters.py +3 -0
  9. janito/cli/core/model_guesser.py +40 -24
  10. janito/cli/main_cli.py +9 -12
  11. janito/cli/prompt_core.py +47 -9
  12. janito/cli/rich_terminal_reporter.py +2 -2
  13. janito/drivers/openai/driver.py +1 -0
  14. janito/drivers/zai/driver.py +1 -0
  15. janito/i18n/it.py +46 -46
  16. janito/llm/agent.py +32 -16
  17. janito/llm/auth_utils.py +14 -5
  18. janito/llm/cancellation_manager.py +63 -0
  19. janito/llm/driver.py +8 -0
  20. janito/llm/enter_cancellation.py +107 -0
  21. janito/plugin_system/__init__.py +10 -0
  22. janito/{plugins → plugin_system}/base.py +5 -2
  23. janito/plugin_system/core_loader.py +217 -0
  24. janito/plugin_system/core_loader_fixed.py +225 -0
  25. janito/plugins/__init__.py +31 -12
  26. janito/plugins/auto_loader.py +12 -11
  27. janito/plugins/auto_loader_fixed.py +12 -11
  28. janito/plugins/builtin.py +15 -1
  29. janito/plugins/core/__init__.py +7 -0
  30. janito/plugins/core/codeanalyzer/__init__.py +43 -0
  31. janito/plugins/core/filemanager/__init__.py +124 -0
  32. janito/plugins/core/filemanager/tools/create_file.py +87 -0
  33. janito/plugins/core/filemanager/tools/replace_text_in_file.py +270 -0
  34. janito/plugins/core/imagedisplay/__init__.py +14 -0
  35. janito/plugins/core/imagedisplay/plugin.py +51 -0
  36. janito/plugins/core/imagedisplay/tools/__init__.py +1 -0
  37. janito/plugins/core/imagedisplay/tools/show_image.py +83 -0
  38. janito/{tools/adapters/local → plugins/core/imagedisplay/tools}/show_image_grid.py +13 -5
  39. janito/plugins/core/system/__init__.py +23 -0
  40. janito/plugins/core_adapter.py +89 -11
  41. janito/plugins/dev/__init__.py +7 -0
  42. janito/plugins/dev/pythondev/__init__.py +37 -0
  43. janito/plugins/dev/visualization/__init__.py +23 -0
  44. janito/plugins/discovery.py +5 -5
  45. janito/plugins/discovery_core.py +14 -9
  46. janito/plugins/example_plugin.py +108 -0
  47. janito/plugins/manager.py +1 -1
  48. janito/plugins/tools/__init__.py +10 -0
  49. janito/{tools/adapters/local → plugins/tools}/ask_user.py +3 -3
  50. janito/plugins/tools/copy_file.py +87 -0
  51. janito/plugins/tools/core_tools_plugin.py +87 -0
  52. janito/plugins/tools/create_directory.py +70 -0
  53. janito/{tools/adapters/local → plugins/tools}/create_file.py +6 -6
  54. janito/plugins/tools/decorators.py +19 -0
  55. janito/plugins/tools/delete_text_in_file.py +134 -0
  56. janito/{tools/adapters/local → plugins/tools}/fetch_url.py +3 -3
  57. janito/plugins/tools/find_files.py +143 -0
  58. janito/plugins/tools/get_file_outline/__init__.py +7 -0
  59. janito/plugins/tools/get_file_outline/core.py +122 -0
  60. janito/plugins/tools/get_file_outline/java_outline.py +47 -0
  61. janito/plugins/tools/get_file_outline/markdown_outline.py +14 -0
  62. janito/plugins/tools/get_file_outline/python_outline.py +303 -0
  63. janito/plugins/tools/get_file_outline/search_outline.py +36 -0
  64. janito/plugins/tools/move_file.py +131 -0
  65. janito/plugins/tools/open_html_in_browser.py +51 -0
  66. janito/plugins/tools/open_url.py +37 -0
  67. janito/plugins/tools/python_code_run.py +172 -0
  68. janito/plugins/tools/python_command_run.py +171 -0
  69. janito/plugins/tools/python_file_run.py +172 -0
  70. janito/plugins/tools/read_chart.py +259 -0
  71. janito/plugins/tools/read_files.py +58 -0
  72. janito/plugins/tools/remove_directory.py +55 -0
  73. janito/plugins/tools/remove_file.py +58 -0
  74. janito/{tools/adapters/local → plugins/tools}/replace_text_in_file.py +4 -4
  75. janito/plugins/tools/run_bash_command.py +183 -0
  76. janito/plugins/tools/run_powershell_command.py +218 -0
  77. janito/plugins/tools/search_text/__init__.py +7 -0
  78. janito/plugins/tools/search_text/core.py +205 -0
  79. janito/plugins/tools/search_text/match_lines.py +67 -0
  80. janito/plugins/tools/search_text/pattern_utils.py +73 -0
  81. janito/plugins/tools/search_text/traverse_directory.py +145 -0
  82. janito/{tools/adapters/local → plugins/tools}/show_image.py +15 -6
  83. janito/plugins/tools/show_image_grid.py +85 -0
  84. janito/plugins/tools/validate_file_syntax/__init__.py +7 -0
  85. janito/plugins/tools/validate_file_syntax/core.py +114 -0
  86. janito/plugins/tools/validate_file_syntax/css_validator.py +35 -0
  87. janito/plugins/tools/validate_file_syntax/html_validator.py +100 -0
  88. janito/plugins/tools/validate_file_syntax/jinja2_validator.py +50 -0
  89. janito/plugins/tools/validate_file_syntax/js_validator.py +27 -0
  90. janito/plugins/tools/validate_file_syntax/json_validator.py +6 -0
  91. janito/plugins/tools/validate_file_syntax/markdown_validator.py +109 -0
  92. janito/plugins/tools/validate_file_syntax/ps1_validator.py +32 -0
  93. janito/plugins/tools/validate_file_syntax/python_validator.py +5 -0
  94. janito/plugins/tools/validate_file_syntax/xml_validator.py +11 -0
  95. janito/plugins/tools/validate_file_syntax/yaml_validator.py +6 -0
  96. janito/plugins/tools/view_file.py +172 -0
  97. janito/plugins/ui/__init__.py +7 -0
  98. janito/plugins/ui/userinterface/__init__.py +16 -0
  99. janito/plugins/ui/userinterface/tools/ask_user.py +110 -0
  100. janito/plugins/web/__init__.py +7 -0
  101. janito/plugins/web/webtools/__init__.py +33 -0
  102. janito/plugins/web/webtools/tools/fetch_url.py +458 -0
  103. janito/providers/__init__.py +1 -0
  104. janito/providers/together/__init__.py +1 -0
  105. janito/providers/together/model_info.py +69 -0
  106. janito/providers/together/provider.py +108 -0
  107. janito/tools/__init__.py +31 -7
  108. janito/tools/adapters/__init__.py +6 -1
  109. janito/tools/adapters/local/__init__.py +7 -70
  110. janito/tools/cli_initializer.py +88 -0
  111. janito/tools/initialize.py +70 -0
  112. janito/tools/loop_protection_decorator.py +114 -117
  113. janito-3.5.0.dist-info/METADATA +229 -0
  114. {janito-3.4.0.dist-info → janito-3.5.0.dist-info}/RECORD +158 -86
  115. janito/plugins/core_loader.py +0 -120
  116. janito/plugins/core_loader_fixed.py +0 -125
  117. janito/tools/function_adapter.py +0 -65
  118. janito-3.4.0.dist-info/METADATA +0 -84
  119. /janito/{tools/adapters/local → plugins/core/codeanalyzer/tools}/get_file_outline/__init__.py +0 -0
  120. /janito/{tools/adapters/local → plugins/core/codeanalyzer/tools}/get_file_outline/core.py +0 -0
  121. /janito/{tools/adapters/local → plugins/core/codeanalyzer/tools}/get_file_outline/java_outline.py +0 -0
  122. /janito/{tools/adapters/local → plugins/core/codeanalyzer/tools}/get_file_outline/markdown_outline.py +0 -0
  123. /janito/{tools/adapters/local → plugins/core/codeanalyzer/tools}/get_file_outline/python_outline.py +0 -0
  124. /janito/{tools/adapters/local → plugins/core/codeanalyzer/tools}/get_file_outline/search_outline.py +0 -0
  125. /janito/{tools/adapters/local → plugins/core/codeanalyzer/tools}/search_text/__init__.py +0 -0
  126. /janito/{tools/adapters/local → plugins/core/codeanalyzer/tools}/search_text/core.py +0 -0
  127. /janito/{tools/adapters/local → plugins/core/codeanalyzer/tools}/search_text/match_lines.py +0 -0
  128. /janito/{tools/adapters/local → plugins/core/codeanalyzer/tools}/search_text/pattern_utils.py +0 -0
  129. /janito/{tools/adapters/local → plugins/core/codeanalyzer/tools}/search_text/traverse_directory.py +0 -0
  130. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/copy_file.py +0 -0
  131. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/create_directory.py +0 -0
  132. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/delete_text_in_file.py +0 -0
  133. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/find_files.py +0 -0
  134. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/move_file.py +0 -0
  135. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/read_files.py +0 -0
  136. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/remove_directory.py +0 -0
  137. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/remove_file.py +0 -0
  138. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/__init__.py +0 -0
  139. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/core.py +0 -0
  140. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/css_validator.py +0 -0
  141. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/html_validator.py +0 -0
  142. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/jinja2_validator.py +0 -0
  143. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/js_validator.py +0 -0
  144. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/json_validator.py +0 -0
  145. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/markdown_validator.py +0 -0
  146. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/ps1_validator.py +0 -0
  147. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/python_validator.py +0 -0
  148. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/xml_validator.py +0 -0
  149. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/yaml_validator.py +0 -0
  150. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/view_file.py +0 -0
  151. /janito/{tools/adapters/local → plugins/core/system/tools}/run_bash_command.py +0 -0
  152. /janito/{tools/adapters/local → plugins/core/system/tools}/run_powershell_command.py +0 -0
  153. /janito/{tools/adapters/local → plugins/dev/pythondev/tools}/python_code_run.py +0 -0
  154. /janito/{tools/adapters/local → plugins/dev/pythondev/tools}/python_command_run.py +0 -0
  155. /janito/{tools/adapters/local → plugins/dev/pythondev/tools}/python_file_run.py +0 -0
  156. /janito/{tools/adapters/local → plugins/dev/visualization/tools}/read_chart.py +0 -0
  157. /janito/{tools/adapters/local → plugins/web/webtools/tools}/open_html_in_browser.py +0 -0
  158. /janito/{tools/adapters/local → plugins/web/webtools/tools}/open_url.py +0 -0
  159. {janito-3.4.0.dist-info → janito-3.5.0.dist-info}/WHEEL +0 -0
  160. {janito-3.4.0.dist-info → janito-3.5.0.dist-info}/entry_points.txt +0 -0
  161. {janito-3.4.0.dist-info → janito-3.5.0.dist-info}/licenses/LICENSE +0 -0
  162. {janito-3.4.0.dist-info → janito-3.5.0.dist-info}/top_level.txt +0 -0
janito/tools/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
- from janito.tools.adapters.local import (
2
- local_tools_adapter as _internal_local_tools_adapter,
3
- LocalToolsAdapter,
1
+ from janito.tools.adapters.local import LocalToolsAdapter
2
+ from janito.tools.adapters.local.adapter import (
3
+ LocalToolsAdapter as _internal_local_tools_adapter,
4
4
  )
5
5
 
6
6
 
@@ -11,8 +11,10 @@ def get_local_tools_adapter(workdir=None, allowed_permissions=None):
11
11
  if workdir is not None and not os.path.exists(workdir):
12
12
  os.makedirs(workdir, exist_ok=True)
13
13
  # Permissions are now managed globally; ignore allowed_permissions argument except for backward compatibility
14
- # Reuse the singleton adapter defined in janito.tools.adapters.local to maintain tool registrations
15
- registry = _internal_local_tools_adapter
14
+ # Create and initialize adapter
15
+ from janito.tools.initialize import initialize_tools
16
+
17
+ registry = initialize_tools(LocalToolsAdapter(workdir=workdir))
16
18
  # Change workdir if requested
17
19
  if workdir is not None:
18
20
  try:
@@ -27,10 +29,32 @@ def get_local_tools_adapter(workdir=None, allowed_permissions=None):
27
29
  return registry
28
30
 
29
31
 
30
- local_tools_adapter = _internal_local_tools_adapter
32
+ # Initialize the global adapter - defer import to avoid circular dependencies
33
+ local_tools_adapter = None
34
+
35
+
36
+ def get_local_tools_adapter(workdir=None, allowed_permissions=None):
37
+ """Get the global tools adapter, initializing on first use."""
38
+ global local_tools_adapter
39
+ if local_tools_adapter is None:
40
+ from janito.tools.initialize import initialize_tools
41
+
42
+ adapter = LocalToolsAdapter(workdir=workdir)
43
+ local_tools_adapter = initialize_tools(adapter)
44
+
45
+ # Handle workdir if provided
46
+ if workdir is not None and local_tools_adapter is not None:
47
+ import os
48
+
49
+ if not os.path.exists(workdir):
50
+ os.makedirs(workdir, exist_ok=True)
51
+ os.chdir(workdir)
52
+ local_tools_adapter.workdir = workdir
53
+
54
+ return local_tools_adapter
55
+
31
56
 
32
57
  __all__ = [
33
58
  "LocalToolsAdapter",
34
59
  "get_local_tools_adapter",
35
- "local_tools_adapter",
36
60
  ]
@@ -1 +1,6 @@
1
- # Tools providers package: for plug-and-play tool collections, integrations, and adapters.
1
+ """
2
+ Tools providers package: for plug-and-play tool collections, integrations, and adapters.
3
+
4
+ This package contains the core adapter infrastructure for managing tools,
5
+ while the actual tool implementations have been moved to the plugins system.
6
+ """
@@ -1,73 +1,10 @@
1
- from .adapter import LocalToolsAdapter
2
-
3
- from .ask_user import AskUserTool
4
- from .copy_file import CopyFileTool
5
- from .create_directory import CreateDirectoryTool
6
- from .create_file import CreateFileTool
7
- from .fetch_url import FetchUrlTool
8
- from .find_files import FindFilesTool
9
- from .view_file import ViewFileTool
10
- from .read_files import ReadFilesTool
11
- from .move_file import MoveFileTool
12
- from .open_url import OpenUrlTool
13
- from .open_html_in_browser import OpenHtmlInBrowserTool
14
- from .python_code_run import PythonCodeRunTool
15
- from .python_command_run import PythonCommandRunTool
16
- from .python_file_run import PythonFileRunTool
17
- from .remove_directory import RemoveDirectoryTool
18
- from .remove_file import RemoveFileTool
19
- from .replace_text_in_file import ReplaceTextInFileTool
20
- from .run_bash_command import RunBashCommandTool
21
- from .run_powershell_command import RunPowershellCommandTool
22
- from .get_file_outline.core import GetFileOutlineTool
23
- from .get_file_outline.search_outline import SearchOutlineTool
24
- from .search_text.core import SearchTextTool
25
- from .validate_file_syntax.core import ValidateFileSyntaxTool
26
- from .read_chart import ReadChartTool
27
- from .show_image import ShowImageTool
28
- from .show_image_grid import ShowImageGridTool
29
-
30
- from janito.tools.tool_base import ToolPermissions
31
- import os
32
- from janito.tools.permissions import get_global_allowed_permissions
33
-
34
- # Singleton tools adapter with all standard tools registered
35
- local_tools_adapter = LocalToolsAdapter(workdir=os.getcwd())
1
+ """
2
+ Local tools adapter for janito.
36
3
 
4
+ This package provides the LocalToolsAdapter class which manages tool registration
5
+ and execution for local, in-process tools.
6
+ """
37
7
 
38
- def get_local_tools_adapter(workdir=None):
39
- return LocalToolsAdapter(workdir=workdir or os.getcwd())
40
-
41
-
42
- # Register tools
43
- for tool_class in [
44
- AskUserTool,
45
- CopyFileTool,
46
- CreateDirectoryTool,
47
- CreateFileTool,
48
- FetchUrlTool,
49
- FindFilesTool,
50
- ViewFileTool,
51
- ReadFilesTool,
52
- MoveFileTool,
53
- OpenUrlTool,
54
- OpenHtmlInBrowserTool,
55
- PythonCodeRunTool,
56
- PythonCommandRunTool,
57
- PythonFileRunTool,
58
- RemoveDirectoryTool,
59
- RemoveFileTool,
60
- ReplaceTextInFileTool,
61
- RunBashCommandTool,
62
- RunPowershellCommandTool,
63
- GetFileOutlineTool,
64
- SearchOutlineTool,
65
- SearchTextTool,
66
- ValidateFileSyntaxTool,
67
- ReadChartTool,
68
- ShowImageTool,
69
- ShowImageGridTool,
70
- ]:
71
- local_tools_adapter.register_tool(tool_class)
8
+ from .adapter import LocalToolsAdapter
72
9
 
73
- # DEBUG: Print registered tools at startup
10
+ __all__ = ["LocalToolsAdapter"]
@@ -0,0 +1,88 @@
1
+ """
2
+ CLI-specific tool initialization for janito.
3
+
4
+ This module provides functions to initialize tools specifically for CLI usage,
5
+ handling circular imports and ensuring proper registration.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import List, Optional
11
+
12
+ # Ensure current directory is in path
13
+ sys.path.insert(0, str(Path.cwd()))
14
+
15
+
16
+ def initialize_cli_tools():
17
+ """Initialize tools for CLI usage, avoiding circular imports."""
18
+ try:
19
+ from janito.tools.adapters.local.adapter import LocalToolsAdapter
20
+ from janito.plugin_system.core_loader_fixed import load_core_plugin
21
+ from janito.tools.permissions import set_global_allowed_permissions
22
+ from janito.tools.tool_base import ToolPermissions
23
+
24
+ # Create adapter
25
+ adapter = LocalToolsAdapter()
26
+
27
+ # Set permissions for CLI
28
+ set_global_allowed_permissions(
29
+ ToolPermissions(read=True, write=True, execute=True)
30
+ )
31
+
32
+ # Core plugins to load
33
+ core_plugins = [
34
+ "core.filemanager",
35
+ "core.codeanalyzer",
36
+ "core.system",
37
+ "core.imagedisplay",
38
+ "dev.pythondev",
39
+ "dev.visualization",
40
+ ]
41
+
42
+ loaded_count = 0
43
+ for plugin_name in core_plugins:
44
+ plugin = load_core_plugin(plugin_name)
45
+ if plugin:
46
+ tools = plugin.get_tools()
47
+ for tool_class in tools:
48
+ try:
49
+ adapter.register_tool(tool_class)
50
+ loaded_count += 1
51
+ except ValueError:
52
+ # Tool already registered, skip
53
+ pass
54
+
55
+ return adapter, loaded_count
56
+
57
+ except Exception as e:
58
+ print(f"Error initializing CLI tools: {e}", file=sys.stderr)
59
+ return None, 0
60
+
61
+
62
+ def get_cli_tools_adapter():
63
+ """Get a CLI-initialized tools adapter."""
64
+ adapter, count = initialize_cli_tools()
65
+ if adapter and count > 0:
66
+ return adapter
67
+ return None
68
+
69
+
70
+ def list_cli_tools():
71
+ """List all available CLI tools."""
72
+ adapter = get_cli_tools_adapter()
73
+ if not adapter:
74
+ return []
75
+
76
+ return adapter.list_tools()
77
+
78
+
79
+ if __name__ == "__main__":
80
+ adapter, count = initialize_cli_tools()
81
+ if adapter:
82
+ tools = adapter.list_tools()
83
+ print(f"CLI initialized {count} tools")
84
+ print(f"Available tools: {len(tools)}")
85
+ for tool in sorted(tools):
86
+ print(f" - {tool}")
87
+ else:
88
+ print("Failed to initialize CLI tools")
@@ -0,0 +1,70 @@
1
+ """
2
+ Initialize janito tools for CLI and programmatic usage.
3
+
4
+ This module provides functions to load core plugins and register their tools
5
+ with the local tools adapter.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import List, Optional
11
+
12
+ from janito.plugin_system.core_loader_fixed import load_core_plugin
13
+ from janito.tools.adapters.local.adapter import LocalToolsAdapter
14
+
15
+
16
+ def initialize_tools(adapter: Optional[LocalToolsAdapter] = None) -> LocalToolsAdapter:
17
+ """
18
+ Initialize all janito tools by loading core plugins and registering tools.
19
+
20
+ Args:
21
+ adapter: LocalToolsAdapter instance to register tools with.
22
+ If None, creates a new instance.
23
+
24
+ Returns:
25
+ LocalToolsAdapter with all tools registered.
26
+ """
27
+ if adapter is None:
28
+ adapter = LocalToolsAdapter()
29
+
30
+ # Core plugins to load
31
+ core_plugins = [
32
+ "core.filemanager",
33
+ "core.codeanalyzer",
34
+ "core.system",
35
+ "core.imagedisplay",
36
+ "dev.pythondev",
37
+ "dev.visualization",
38
+ ]
39
+
40
+ loaded_count = 0
41
+ for plugin_name in core_plugins:
42
+ plugin = load_core_plugin(plugin_name)
43
+ if plugin:
44
+ tools = plugin.get_tools()
45
+ for tool_class in tools:
46
+ adapter.register_tool(tool_class)
47
+ loaded_count += 1
48
+
49
+ return adapter
50
+
51
+
52
+ def get_initialized_adapter() -> LocalToolsAdapter:
53
+ """Get a pre-initialized LocalToolsAdapter with all tools registered."""
54
+ return initialize_tools()
55
+
56
+
57
+ def list_available_tools() -> List[str]:
58
+ """Get a list of all available tool names."""
59
+ adapter = get_initialized_adapter()
60
+ return sorted(list(adapter._tools.keys()))
61
+
62
+
63
+ if __name__ == "__main__":
64
+ adapter = initialize_tools()
65
+ tools = sorted(list(adapter._tools.keys()))
66
+
67
+ print(f"Initialized {len(tools)} tools")
68
+ print("Available tools:")
69
+ for tool_name in tools:
70
+ print(f" - {tool_name}")
@@ -1,143 +1,140 @@
1
1
  import functools
2
2
  import time
3
3
  import threading
4
- from typing import Callable, Any
5
- from janito.tools.loop_protection import LoopProtection
6
- from janito.tools.tool_use_tracker import normalize_path
7
-
4
+ from typing import Any, Tuple
8
5
 
9
6
  # Global tracking for decorator-based loop protection
10
7
  _decorator_call_tracker = {}
11
8
  _decorator_call_tracker_lock = threading.Lock()
12
9
 
13
10
 
11
+ def _normalize_key_value(key_field: str, key_value: Any) -> Any:
12
+ """Normalize key values, especially paths, so different representations map to the same key."""
13
+ if key_value is None:
14
+ return None
15
+
16
+ try:
17
+ if isinstance(key_field, str) and "path" in key_field.lower():
18
+ from janito.tools.tool_use_tracker import (
19
+ normalize_path as _norm, # reuse existing normalization
20
+ )
21
+
22
+ if isinstance(key_value, str):
23
+ return _norm(key_value)
24
+ if isinstance(key_value, (list, tuple)):
25
+ return tuple(_norm(v) if isinstance(v, str) else v for v in key_value)
26
+ except Exception:
27
+ # Best-effort normalization – fall back to original value
28
+ pass
29
+
30
+ return key_value
31
+
32
+
33
+ def _get_param_value(func, args, kwargs, key_field: str):
34
+ """Extract the watched parameter value from args/kwargs using function signature."""
35
+ if key_field in kwargs:
36
+ return kwargs[key_field]
37
+
38
+ # Handle positional arguments by mapping to parameter names
39
+ if len(args) > 1: # args[0] is self
40
+ import inspect
41
+
42
+ try:
43
+ sig = inspect.signature(func)
44
+ param_names = list(sig.parameters.keys())
45
+ if key_field in param_names:
46
+ idx = param_names.index(key_field)
47
+ if idx < len(args):
48
+ return args[idx]
49
+ except Exception:
50
+ return None
51
+
52
+ return None
53
+
54
+
55
+ def _determine_operation_name(func, args, kwargs, key_field: str) -> str:
56
+ """Build the operation name for rate limiting, optionally including a normalized key value."""
57
+ if key_field:
58
+ raw_value = _get_param_value(func, args, kwargs, key_field)
59
+ if raw_value is not None:
60
+ norm_value = _normalize_key_value(key_field, raw_value)
61
+ return f"{func.__name__}_{norm_value}"
62
+ return func.__name__
63
+
64
+
65
+ def _check_and_record(
66
+ op_name: str,
67
+ current_time: float,
68
+ time_window: float,
69
+ max_calls: int,
70
+ tool_instance: Any,
71
+ ) -> Tuple[bool, str]:
72
+ """Check loop limits for op_name and record the call. Returns (exceeded, message)."""
73
+ with _decorator_call_tracker_lock:
74
+ # Clean old timestamps
75
+ if op_name in _decorator_call_tracker:
76
+ _decorator_call_tracker[op_name] = [
77
+ ts
78
+ for ts in _decorator_call_tracker[op_name]
79
+ if current_time - ts <= time_window
80
+ ]
81
+
82
+ # Check limit
83
+ if (
84
+ op_name in _decorator_call_tracker
85
+ and len(_decorator_call_tracker[op_name]) >= max_calls
86
+ ):
87
+ if all(
88
+ current_time - ts <= time_window
89
+ for ts in _decorator_call_tracker[op_name]
90
+ ):
91
+ msg = (
92
+ f"Loop protection: Too many {op_name} operations in a short time period "
93
+ f"({max_calls} calls in {time_window}s). Please try a different approach or wait before retrying."
94
+ )
95
+ if hasattr(tool_instance, "report_error"):
96
+ try:
97
+ tool_instance.report_error(msg)
98
+ except Exception:
99
+ pass
100
+ return True, msg
101
+
102
+ # Record this call
103
+ if op_name not in _decorator_call_tracker:
104
+ _decorator_call_tracker[op_name] = []
105
+ _decorator_call_tracker[op_name].append(current_time)
106
+
107
+ return False, ""
108
+
109
+
14
110
  def protect_against_loops(
15
111
  max_calls: int = 5, time_window: float = 10.0, key_field: str = None
16
112
  ):
17
113
  """
18
114
  Decorator that adds loop protection to tool run methods.
19
115
 
20
- This decorator monitors tool executions and prevents excessive calls within
21
- a configurable time window. It helps prevent infinite loops or excessive
22
- resource consumption when tools are called repeatedly.
23
-
24
- When the configured limits are exceeded, the decorator raises a RuntimeError
25
- with a descriptive message. This exception will propagate up the call stack
26
- unless caught by a try/except block in the calling code.
27
-
28
- The decorator works by:
29
- 1. Tracking the number of calls to the decorated function
30
- 2. Checking if the calls exceed the configured limits
31
- 3. Raising a RuntimeError if a potential loop is detected
32
- 4. Allowing the method to proceed normally if the operation is safe
33
-
34
- Args:
35
- max_calls (int): Maximum number of calls allowed within the time window.
36
- Defaults to 5 calls.
37
- time_window (float): Time window in seconds for detecting excessive calls.
38
- Defaults to 10.0 seconds.
39
- key_field (str, optional): The parameter name to use for key matching instead of function name.
40
- If provided, the decorator will track calls based on the value of this
41
- parameter rather than the function name. Useful for tools that operate
42
- on specific files or resources.
43
-
44
- Example:
45
- >>> @protect_against_loops(max_calls=3, time_window=5.0)
46
- >>> def run(self, path: str) -> str:
47
- >>> # Implementation here
48
- >>> pass
49
-
50
- >>> @protect_against_loops(max_calls=10, time_window=30.0)
51
- >>> def run(self, file_paths: list) -> str:
52
- >>> # Implementation here
53
- >>> pass
54
-
55
- >>> @protect_against_loops(max_calls=5, time_window=10.0, key_field='path')
56
- >>> def run(self, path: str) -> str:
57
- >>> # This will track calls per unique path value
58
- >>> pass
59
-
60
- Note:
61
- When loop protection is triggered, a RuntimeError will be raised with a
62
- descriptive message. This exception will propagate up the call stack
63
- unless caught by a try/except block in the calling code.
116
+ Tracks calls within a sliding time window and prevents excessive repeated operations.
117
+ When key_field is provided, the limit is applied per unique normalized value of that parameter
118
+ (e.g., per-path protection for file tools).
64
119
  """
65
120
 
66
121
  def decorator(func):
67
122
  @functools.wraps(func)
68
123
  def wrapper(*args, **kwargs):
69
- # Get the tool instance (self)
124
+ # Methods should always have self; if not, execute directly.
70
125
  if not args:
71
- # This shouldn't happen in normal usage as methods need self
72
126
  return func(*args, **kwargs)
73
127
 
74
- # Determine the operation key
75
- if key_field:
76
- # Use the key_field parameter value as the operation key
77
- key_value = None
78
- if key_field in kwargs:
79
- key_value = kwargs[key_field]
80
- elif len(args) > 1:
81
- # Handle positional arguments - need to map parameter names
82
- import inspect
83
-
84
- try:
85
- sig = inspect.signature(func)
86
- param_names = list(sig.parameters.keys())
87
- if key_field in param_names:
88
- field_index = param_names.index(key_field)
89
- if field_index < len(args):
90
- key_value = args[field_index]
91
- except (ValueError, TypeError):
92
- pass
93
-
94
- if key_value is not None:
95
- op_name = f"{func.__name__}_{key_value}"
96
- else:
97
- op_name = func.__name__
98
- else:
99
- # Use the function name as the operation name
100
- op_name = func.__name__
101
-
102
- # Check call limits
103
- current_time = time.time()
104
-
105
- with _decorator_call_tracker_lock:
106
- # Clean up old entries outside the time window
107
- if op_name in _decorator_call_tracker:
108
- _decorator_call_tracker[op_name] = [
109
- timestamp
110
- for timestamp in _decorator_call_tracker[op_name]
111
- if current_time - timestamp <= time_window
112
- ]
113
-
114
- # Check if we're exceeding the limit
115
- if op_name in _decorator_call_tracker:
116
- if len(_decorator_call_tracker[op_name]) >= max_calls:
117
- # Check if all recent calls are within the time window
118
- if all(
119
- current_time - timestamp <= time_window
120
- for timestamp in _decorator_call_tracker[op_name]
121
- ):
122
- # Return loop protection message as string instead of raising exception
123
- error_msg = f"Loop protection: Too many {op_name} operations in a short time period ({max_calls} calls in {time_window}s). Please try a different approach or wait before retrying."
124
-
125
- # Try to report the error through the tool's reporting mechanism
126
- tool_instance = args[0] if args else None
127
- if hasattr(tool_instance, "report_error"):
128
- try:
129
- tool_instance.report_error(error_msg)
130
- except Exception:
131
- pass # If reporting fails, we still return the message
132
-
133
- return error_msg
134
-
135
- # Record this call
136
- if op_name not in _decorator_call_tracker:
137
- _decorator_call_tracker[op_name] = []
138
- _decorator_call_tracker[op_name].append(current_time)
139
-
140
- # Proceed with the original function
128
+ op_name = _determine_operation_name(func, args, kwargs, key_field)
129
+ exceeded, msg = _check_and_record(
130
+ op_name=op_name,
131
+ current_time=time.time(),
132
+ time_window=time_window,
133
+ max_calls=max_calls,
134
+ tool_instance=args[0],
135
+ )
136
+ if exceeded:
137
+ return msg
141
138
  return func(*args, **kwargs)
142
139
 
143
140
  return wrapper