janito 2.31.0__py3-none-any.whl → 2.32.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.
- janito/cli/chat_mode/session.py +0 -2
- janito/cli/cli_commands/list_plugins.py +131 -20
- janito/cli/prompt_core.py +0 -2
- janito/cli/rich_terminal_reporter.py +1 -1
- janito/cli/single_shot_mode/handler.py +0 -2
- janito/plugins/auto_loader.py +91 -0
- janito/plugins/auto_loader_fixed.py +90 -0
- janito/plugins/core_adapter.py +53 -0
- janito/plugins/core_loader.py +120 -0
- janito/plugins/core_loader_fixed.py +125 -0
- janito/plugins/discovery.py +8 -0
- janito/plugins/discovery_core.py +49 -0
- janito/provider_registry.py +1 -1
- janito/providers/deepseek/model_info.py +21 -0
- janito/providers/deepseek/provider.py +1 -1
- janito/providers/openai/provider.py +1 -1
- janito/tools/adapters/local/__init__.py +4 -0
- janito/tools/adapters/local/create_file.py +60 -9
- janito/tools/adapters/local/fetch_url.py +35 -6
- janito/tools/adapters/local/search_text/core.py +1 -1
- janito/tools/adapters/local/show_image.py +74 -0
- janito/tools/adapters/local/show_image_grid.py +76 -0
- janito/tools/function_adapter.py +65 -0
- janito-2.32.0.dist-info/METADATA +84 -0
- {janito-2.31.0.dist-info → janito-2.32.0.dist-info}/RECORD +29 -20
- janito-2.32.0.dist-info/licenses/LICENSE +201 -0
- janito-2.31.0.dist-info/METADATA +0 -431
- janito-2.31.0.dist-info/licenses/LICENSE +0 -21
- {janito-2.31.0.dist-info → janito-2.32.0.dist-info}/WHEEL +0 -0
- {janito-2.31.0.dist-info → janito-2.32.0.dist-info}/entry_points.txt +0 -0
- {janito-2.31.0.dist-info → janito-2.32.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
"""
|
2
|
+
Fixed core plugin loader.
|
3
|
+
|
4
|
+
This module provides a working implementation to load core plugins
|
5
|
+
by directly using the Plugin base class properly.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import importlib.util
|
9
|
+
import sys
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Optional, List, Type
|
12
|
+
|
13
|
+
from janito.plugins.base import Plugin, PluginMetadata
|
14
|
+
from janito.tools.function_adapter import create_function_tool
|
15
|
+
from janito.tools.tool_base import ToolBase
|
16
|
+
|
17
|
+
|
18
|
+
class CorePlugin(Plugin):
|
19
|
+
"""Working core plugin implementation."""
|
20
|
+
|
21
|
+
def __init__(self, name: str, description: str, tools: list):
|
22
|
+
self._plugin_name = name
|
23
|
+
self._description = description
|
24
|
+
self._tools = tools
|
25
|
+
self._tool_classes = []
|
26
|
+
super().__init__() # Call super after setting attributes
|
27
|
+
|
28
|
+
def get_metadata(self) -> PluginMetadata:
|
29
|
+
return PluginMetadata(
|
30
|
+
name=self._plugin_name,
|
31
|
+
version="1.0.0",
|
32
|
+
description=self._description,
|
33
|
+
author="Janito",
|
34
|
+
license="MIT",
|
35
|
+
)
|
36
|
+
|
37
|
+
def get_tools(self) -> List[Type[ToolBase]]:
|
38
|
+
return self._tool_classes
|
39
|
+
|
40
|
+
def initialize(self):
|
41
|
+
"""Initialize by creating tool classes."""
|
42
|
+
self._tool_classes = []
|
43
|
+
for tool_func in self._tools:
|
44
|
+
if callable(tool_func):
|
45
|
+
tool_class = create_function_tool(tool_func)
|
46
|
+
self._tool_classes.append(tool_class)
|
47
|
+
|
48
|
+
|
49
|
+
def load_core_plugin(plugin_name: str) -> Optional[Plugin]:
|
50
|
+
"""
|
51
|
+
Load a core plugin by name.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
plugin_name: Name of the plugin (e.g., 'core.filemanager')
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
Plugin instance if loaded successfully
|
58
|
+
"""
|
59
|
+
try:
|
60
|
+
# Parse plugin name
|
61
|
+
if "." not in plugin_name:
|
62
|
+
return None
|
63
|
+
|
64
|
+
parts = plugin_name.split(".")
|
65
|
+
if len(parts) != 2:
|
66
|
+
return None
|
67
|
+
|
68
|
+
package_name, submodule_name = parts
|
69
|
+
|
70
|
+
# Handle imagedisplay specially
|
71
|
+
if plugin_name == "core.imagedisplay":
|
72
|
+
# Import the actual plugin class
|
73
|
+
try:
|
74
|
+
from plugins.core.imagedisplay.plugin import ImageDisplayPlugin
|
75
|
+
return ImageDisplayPlugin()
|
76
|
+
except ImportError:
|
77
|
+
# If import fails, return None - don't return True
|
78
|
+
return None
|
79
|
+
|
80
|
+
# Build path to plugin
|
81
|
+
plugin_path = Path("plugins") / package_name / submodule_name / "__init__.py"
|
82
|
+
if not plugin_path.exists():
|
83
|
+
return None
|
84
|
+
|
85
|
+
# Load the module
|
86
|
+
spec = importlib.util.spec_from_file_location(plugin_name, plugin_path)
|
87
|
+
if spec is None or spec.loader is None:
|
88
|
+
return None
|
89
|
+
|
90
|
+
module = importlib.util.module_from_spec(spec)
|
91
|
+
spec.loader.exec_module(module)
|
92
|
+
|
93
|
+
# Get plugin info
|
94
|
+
name = getattr(module, "__plugin_name__", plugin_name)
|
95
|
+
description = getattr(module, "__plugin_description__", f"Core plugin: {plugin_name}")
|
96
|
+
tools = getattr(module, "__plugin_tools__", [])
|
97
|
+
|
98
|
+
if not tools:
|
99
|
+
return None
|
100
|
+
|
101
|
+
# Create plugin
|
102
|
+
plugin = CorePlugin(name, description, tools)
|
103
|
+
plugin.initialize()
|
104
|
+
return plugin
|
105
|
+
|
106
|
+
except Exception as e:
|
107
|
+
print(f"Error loading core plugin {plugin_name}: {e}")
|
108
|
+
return None
|
109
|
+
|
110
|
+
|
111
|
+
def get_core_plugins() -> list:
|
112
|
+
"""Get list of all available core plugins."""
|
113
|
+
core_plugins = [
|
114
|
+
"core.filemanager",
|
115
|
+
"core.codeanalyzer",
|
116
|
+
"core.system",
|
117
|
+
"core.imagedisplay",
|
118
|
+
"dev.pythondev",
|
119
|
+
"dev.visualization",
|
120
|
+
"ui.userinterface",
|
121
|
+
"web.webtools",
|
122
|
+
]
|
123
|
+
|
124
|
+
# All core plugins are always available
|
125
|
+
return core_plugins
|
janito/plugins/discovery.py
CHANGED
@@ -33,6 +33,7 @@ import logging
|
|
33
33
|
|
34
34
|
from .base import Plugin
|
35
35
|
from .builtin import load_builtin_plugin, BuiltinPluginRegistry
|
36
|
+
from .core_loader import load_core_plugin
|
36
37
|
|
37
38
|
logger = logging.getLogger(__name__)
|
38
39
|
|
@@ -74,6 +75,13 @@ def discover_plugins(
|
|
74
75
|
parts = plugin_name.split(".")
|
75
76
|
if len(parts) == 2:
|
76
77
|
package_name, submodule_name = parts
|
78
|
+
|
79
|
+
# Handle core plugins with dedicated loader
|
80
|
+
if plugin_name.startswith(("core.", "dev.", "ui.", "web.")):
|
81
|
+
plugin = load_core_plugin(plugin_name)
|
82
|
+
if plugin:
|
83
|
+
return plugin
|
84
|
+
|
77
85
|
for base_path in all_paths:
|
78
86
|
package_path = base_path / package_name / submodule_name / "__init__.py"
|
79
87
|
if package_path.exists():
|
@@ -0,0 +1,49 @@
|
|
1
|
+
"""
|
2
|
+
Core plugin discovery utilities.
|
3
|
+
|
4
|
+
This module provides specialized handling for core plugins that use
|
5
|
+
the function-based approach instead of class-based plugins.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import importlib.util
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Optional
|
11
|
+
import sys
|
12
|
+
|
13
|
+
from .base import Plugin
|
14
|
+
from .core_adapter import CorePluginAdapter
|
15
|
+
|
16
|
+
|
17
|
+
def _load_core_plugin(package_path: Path, plugin_name: str) -> Optional[Plugin]:
|
18
|
+
"""
|
19
|
+
Load a core plugin from a package directory.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
package_path: Path to the __init__.py file
|
23
|
+
plugin_name: Full plugin name (e.g., core.filemanager)
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
Plugin instance if loaded successfully
|
27
|
+
"""
|
28
|
+
try:
|
29
|
+
# Import the module
|
30
|
+
spec = importlib.util.spec_from_file_location(plugin_name, package_path)
|
31
|
+
if spec is None or spec.loader is None:
|
32
|
+
return None
|
33
|
+
|
34
|
+
module = importlib.util.module_from_spec(spec)
|
35
|
+
spec.loader.exec_module(module)
|
36
|
+
|
37
|
+
# Get plugin metadata
|
38
|
+
plugin_name_attr = getattr(module, "__plugin_name__", plugin_name)
|
39
|
+
description = getattr(module, "__plugin_description__", f"Core plugin: {plugin_name}")
|
40
|
+
|
41
|
+
# Create and return the core plugin adapter
|
42
|
+
plugin = CorePluginAdapter(plugin_name_attr, description, module)
|
43
|
+
plugin.initialize() # Initialize to set up tools
|
44
|
+
return plugin
|
45
|
+
|
46
|
+
except Exception as e:
|
47
|
+
import logging
|
48
|
+
logging.getLogger(__name__).error(f"Failed to load core plugin {plugin_name}: {e}")
|
49
|
+
return None
|
janito/provider_registry.py
CHANGED
@@ -41,7 +41,7 @@ class ProviderRegistry:
|
|
41
41
|
rows.append(info[:3])
|
42
42
|
|
43
43
|
# Group providers by openness (open-source first, then proprietary)
|
44
|
-
open_providers = {"cerebras", "deepseek", "alibaba", "
|
44
|
+
open_providers = {"cerebras", "deepseek", "alibaba", "moonshot", "zai"}
|
45
45
|
|
46
46
|
def sort_key(row):
|
47
47
|
provider_name = row[0]
|
@@ -13,4 +13,25 @@ MODEL_SPECS = {
|
|
13
13
|
"family": "deepseek",
|
14
14
|
"default": False,
|
15
15
|
},
|
16
|
+
"deepseek-v3.1": {
|
17
|
+
"description": "DeepSeek V3.1 Model (128K context, OpenAI-compatible)",
|
18
|
+
"context_window": 131072,
|
19
|
+
"max_tokens": 4096,
|
20
|
+
"family": "deepseek",
|
21
|
+
"default": False,
|
22
|
+
},
|
23
|
+
"deepseek-v3.1-base": {
|
24
|
+
"description": "DeepSeek V3.1 Base Model (128K context, OpenAI-compatible)",
|
25
|
+
"context_window": 131072,
|
26
|
+
"max_tokens": 4096,
|
27
|
+
"family": "deepseek",
|
28
|
+
"default": False,
|
29
|
+
},
|
30
|
+
"deepseek-r1": {
|
31
|
+
"description": "DeepSeek R1 Model (128K context, OpenAI-compatible)",
|
32
|
+
"context_window": 131072,
|
33
|
+
"max_tokens": 4096,
|
34
|
+
"family": "deepseek",
|
35
|
+
"default": False,
|
36
|
+
},
|
16
37
|
}
|
@@ -17,7 +17,7 @@ class DeepSeekProvider(LLMProvider):
|
|
17
17
|
NAME = "deepseek"
|
18
18
|
MAINTAINER = "João Pinto <janito@ikignosis.org>"
|
19
19
|
MODEL_SPECS = MODEL_SPECS
|
20
|
-
DEFAULT_MODEL = "deepseek-chat" # Options: deepseek-chat, deepseek-reasoner
|
20
|
+
DEFAULT_MODEL = "deepseek-chat" # Options: deepseek-chat, deepseek-reasoner, deepseek-v3.1, deepseek-v3.1-base, deepseek-r1
|
21
21
|
|
22
22
|
def __init__(
|
23
23
|
self, auth_manager: LLMAuthManager = None, config: LLMDriverConfig = None
|
@@ -18,7 +18,7 @@ class OpenAIProvider(LLMProvider):
|
|
18
18
|
MAINTAINER = "João Pinto <janito@ikignosis.org>"
|
19
19
|
MODEL_SPECS = MODEL_SPECS
|
20
20
|
DEFAULT_MODEL = (
|
21
|
-
"gpt-
|
21
|
+
"gpt-5" # Options: gpt-4.1, gpt-4o, o3-mini, o4-mini, gpt-5, gpt-5-nano
|
22
22
|
)
|
23
23
|
|
24
24
|
def __init__(
|
@@ -24,6 +24,8 @@ from .get_file_outline.search_outline import SearchOutlineTool
|
|
24
24
|
from .search_text.core import SearchTextTool
|
25
25
|
from .validate_file_syntax.core import ValidateFileSyntaxTool
|
26
26
|
from .read_chart import ReadChartTool
|
27
|
+
from .show_image import ShowImageTool
|
28
|
+
from .show_image_grid import ShowImageGridTool
|
27
29
|
|
28
30
|
from janito.tools.tool_base import ToolPermissions
|
29
31
|
import os
|
@@ -63,6 +65,8 @@ for tool_class in [
|
|
63
65
|
SearchTextTool,
|
64
66
|
ValidateFileSyntaxTool,
|
65
67
|
ReadChartTool,
|
68
|
+
ShowImageTool,
|
69
|
+
ShowImageGridTool,
|
66
70
|
]:
|
67
71
|
local_tools_adapter.register_tool(tool_class)
|
68
72
|
|
@@ -14,20 +14,71 @@ from janito.tools.adapters.local.validate_file_syntax.core import validate_file_
|
|
14
14
|
@register_local_tool
|
15
15
|
class CreateFileTool(ToolBase):
|
16
16
|
"""
|
17
|
-
Create a new file with the given
|
17
|
+
Create a new file with specified content at the given path.
|
18
|
+
|
19
|
+
This tool provides comprehensive file creation capabilities with built-in safety features,
|
20
|
+
automatic syntax validation, and detailed feedback. It handles path expansion, directory
|
21
|
+
creation, encoding issues, and provides clear status messages for both success and failure cases.
|
22
|
+
|
23
|
+
Key Features:
|
24
|
+
- Automatic directory creation for nested paths
|
25
|
+
- UTF-8 encoding with error handling for special characters
|
26
|
+
- Built-in syntax validation for common file types (Python, JavaScript, JSON, YAML, etc.)
|
27
|
+
- Loop protection to prevent excessive file creation
|
28
|
+
- Detailed error messages with context
|
29
|
+
- Safe overwrite protection with preview of existing content
|
30
|
+
- Cross-platform path handling (Windows, macOS, Linux)
|
18
31
|
|
19
32
|
Args:
|
20
|
-
path (str):
|
21
|
-
|
22
|
-
|
33
|
+
path (str): Target file path. Supports relative and absolute paths, with automatic
|
34
|
+
expansion of user home directory (~) and environment variables.
|
35
|
+
Examples: "src/main.py", "~/Documents/config.json", "$HOME/.env"
|
36
|
+
content (str): File content to write. Empty string creates empty file.
|
37
|
+
Supports any text content including Unicode characters, newlines,
|
38
|
+
and binary-safe text representation.
|
39
|
+
overwrite (bool, optional): If True, allows overwriting existing files. Default: False.
|
40
|
+
When False, prevents accidental overwrites by checking
|
41
|
+
file existence and showing current content. Always review
|
42
|
+
existing content before enabling overwrite.
|
43
|
+
|
23
44
|
Returns:
|
24
|
-
str:
|
25
|
-
-
|
45
|
+
str: Detailed status message including:
|
46
|
+
- Success confirmation with line count
|
47
|
+
- File path (display-friendly format)
|
48
|
+
- Syntax validation results
|
49
|
+
- Existing content preview (when overwrite blocked)
|
50
|
+
- Error details (when creation fails)
|
51
|
+
|
52
|
+
Raises:
|
53
|
+
No direct exceptions - all errors are caught and returned as user-friendly messages.
|
54
|
+
Common error cases include: permission denied, invalid path format, disk full,
|
55
|
+
or file exists (when overwrite=False).
|
56
|
+
|
57
|
+
Security Features:
|
58
|
+
- Loop protection: Maximum 5 calls per 10 seconds for the same file path
|
59
|
+
- Path traversal prevention: Validates and sanitizes file paths
|
60
|
+
- Permission checking: Respects file system permissions
|
61
|
+
- Atomic writes: Prevents partial file creation on errors
|
62
|
+
|
63
|
+
Examples:
|
64
|
+
Basic file creation:
|
65
|
+
>>> create_file("hello.py", "print('Hello, World!')")
|
66
|
+
✅ Created file 1 lines.
|
67
|
+
✅ Syntax OK
|
68
|
+
|
69
|
+
Creating nested directories:
|
70
|
+
>>> create_file("src/utils/helpers.py", "def helper(): pass")
|
71
|
+
✅ Created file 2 lines.
|
72
|
+
✅ Syntax OK
|
26
73
|
|
27
|
-
|
74
|
+
Overwrite protection:
|
75
|
+
>>> create_file("existing.txt", "new content")
|
76
|
+
❗ Cannot create file: file already exists at 'existing.txt'.
|
77
|
+
--- Current file content ---
|
78
|
+
old content
|
28
79
|
|
29
|
-
|
30
|
-
|
80
|
+
Note: After successful creation, automatic syntax validation is performed based on
|
81
|
+
file extension. Results are appended to the return message for immediate feedback.
|
31
82
|
"""
|
32
83
|
|
33
84
|
permissions = ToolPermissions(write=True)
|
@@ -246,10 +246,35 @@ class FetchUrlTool(ToolBase):
|
|
246
246
|
return content
|
247
247
|
except requests.exceptions.HTTPError as http_err:
|
248
248
|
status_code = http_err.response.status_code if http_err.response else None
|
249
|
+
|
250
|
+
# Map status codes to descriptions
|
251
|
+
status_descriptions = {
|
252
|
+
400: "Bad Request",
|
253
|
+
401: "Unauthorized",
|
254
|
+
403: "Forbidden",
|
255
|
+
404: "Not Found",
|
256
|
+
405: "Method Not Allowed",
|
257
|
+
408: "Request Timeout",
|
258
|
+
409: "Conflict",
|
259
|
+
410: "Gone",
|
260
|
+
413: "Payload Too Large",
|
261
|
+
414: "URI Too Long",
|
262
|
+
415: "Unsupported Media Type",
|
263
|
+
429: "Too Many Requests",
|
264
|
+
500: "Internal Server Error",
|
265
|
+
501: "Not Implemented",
|
266
|
+
502: "Bad Gateway",
|
267
|
+
503: "Service Unavailable",
|
268
|
+
504: "Gateway Timeout",
|
269
|
+
505: "HTTP Version Not Supported"
|
270
|
+
}
|
271
|
+
|
249
272
|
if status_code and 400 <= status_code < 500:
|
273
|
+
description = status_descriptions.get(status_code, "Client Error")
|
250
274
|
error_message = tr(
|
251
|
-
"HTTP {status_code}",
|
275
|
+
"HTTP Error {status_code} {description}",
|
252
276
|
status_code=status_code,
|
277
|
+
description=description,
|
253
278
|
)
|
254
279
|
# Cache 403 and 404 errors
|
255
280
|
if status_code in [403, 404]:
|
@@ -257,23 +282,27 @@ class FetchUrlTool(ToolBase):
|
|
257
282
|
|
258
283
|
self.report_error(
|
259
284
|
tr(
|
260
|
-
"❗ HTTP {status_code}",
|
285
|
+
"❗ HTTP Error {status_code} {description}",
|
261
286
|
status_code=status_code,
|
287
|
+
description=description,
|
262
288
|
),
|
263
289
|
ReportAction.READ,
|
264
290
|
)
|
265
291
|
return error_message
|
266
292
|
else:
|
293
|
+
description = status_descriptions.get(status_code, "Server Error") if status_code else "Error"
|
267
294
|
self.report_error(
|
268
295
|
tr(
|
269
|
-
"❗ HTTP {status_code}",
|
296
|
+
"❗ HTTP Error {status_code} {description}",
|
270
297
|
status_code=status_code or "Error",
|
298
|
+
description=description,
|
271
299
|
),
|
272
300
|
ReportAction.READ,
|
273
301
|
)
|
274
302
|
return tr(
|
275
|
-
"HTTP {status_code}",
|
303
|
+
"HTTP Error {status_code} {description}",
|
276
304
|
status_code=status_code or "Error",
|
305
|
+
description=description,
|
277
306
|
)
|
278
307
|
except Exception as err:
|
279
308
|
self.report_error(
|
@@ -355,7 +384,7 @@ class FetchUrlTool(ToolBase):
|
|
355
384
|
follow_redirects=follow_redirects,
|
356
385
|
)
|
357
386
|
if (
|
358
|
-
html_content.startswith("HTTP ")
|
387
|
+
html_content.startswith("HTTP Error ")
|
359
388
|
or html_content == "Error"
|
360
389
|
or html_content == "Blocked"
|
361
390
|
):
|
@@ -388,7 +417,7 @@ class FetchUrlTool(ToolBase):
|
|
388
417
|
follow_redirects=follow_redirects,
|
389
418
|
)
|
390
419
|
if (
|
391
|
-
html_content.startswith("HTTP ")
|
420
|
+
html_content.startswith("HTTP Error ")
|
392
421
|
or html_content == "Error"
|
393
422
|
or html_content == "Blocked"
|
394
423
|
):
|
@@ -98,7 +98,7 @@ class SearchTextTool(ToolBase):
|
|
98
98
|
if max_depth > 0:
|
99
99
|
info_str += tr(" [max_depth={max_depth}]", max_depth=max_depth)
|
100
100
|
if count_only:
|
101
|
-
info_str += " [count
|
101
|
+
info_str += " [count]"
|
102
102
|
self.report_action(info_str, ReportAction.READ)
|
103
103
|
if os.path.isfile(search_path):
|
104
104
|
dir_output, dir_limit_reached, per_file_counts = self._handle_file(
|
@@ -0,0 +1,74 @@
|
|
1
|
+
from janito.tools.tool_base import ToolBase, ToolPermissions
|
2
|
+
from janito.report_events import ReportAction
|
3
|
+
from janito.tools.adapters.local.adapter import register_local_tool
|
4
|
+
from janito.i18n import tr
|
5
|
+
from janito.tools.loop_protection_decorator import protect_against_loops
|
6
|
+
|
7
|
+
|
8
|
+
@register_local_tool
|
9
|
+
class ShowImageTool(ToolBase):
|
10
|
+
"""Display an image inline in the terminal using the rich library.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
path (str): Path to the image file.
|
14
|
+
width (int, optional): Target width in terminal cells. If unset, auto-fit.
|
15
|
+
height (int, optional): Target height in terminal rows. If unset, auto-fit.
|
16
|
+
preserve_aspect (bool, optional): Preserve aspect ratio. Default: True.
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
str: Status message indicating display result or error details.
|
20
|
+
"""
|
21
|
+
|
22
|
+
permissions = ToolPermissions(read=True)
|
23
|
+
tool_name = "show_image"
|
24
|
+
|
25
|
+
@protect_against_loops(max_calls=5, time_window=10.0, key_field="path")
|
26
|
+
def run(
|
27
|
+
self,
|
28
|
+
path: str,
|
29
|
+
width: int | None = None,
|
30
|
+
height: int | None = None,
|
31
|
+
preserve_aspect: bool = True,
|
32
|
+
) -> str:
|
33
|
+
from janito.tools.tool_utils import display_path
|
34
|
+
from janito.tools.path_utils import expand_path
|
35
|
+
import os
|
36
|
+
|
37
|
+
try:
|
38
|
+
from rich.console import Console
|
39
|
+
from PIL import Image as PILImage
|
40
|
+
except Exception as e:
|
41
|
+
msg = tr("⚠️ Missing dependency: PIL/Pillow ({error})", error=e)
|
42
|
+
self.report_error(msg)
|
43
|
+
return msg
|
44
|
+
|
45
|
+
path = expand_path(path)
|
46
|
+
disp_path = display_path(path)
|
47
|
+
self.report_action(tr("🖼️ Show image '{disp_path}'", disp_path=disp_path), ReportAction.READ)
|
48
|
+
|
49
|
+
if not os.path.exists(path):
|
50
|
+
msg = tr("❗ not found")
|
51
|
+
self.report_warning(msg)
|
52
|
+
return tr("Error: file not found: {path}", path=disp_path)
|
53
|
+
|
54
|
+
try:
|
55
|
+
console = Console()
|
56
|
+
from rich.console import Console
|
57
|
+
from rich.text import Text
|
58
|
+
console = Console()
|
59
|
+
img = PILImage.open(path)
|
60
|
+
console.print(Text(f"Image: {disp_path} ({img.width}x{img.height})", style="bold green"))
|
61
|
+
console.print(img)
|
62
|
+
self.report_success(tr("✅ Displayed"))
|
63
|
+
details = []
|
64
|
+
if width:
|
65
|
+
details.append(f"width={width}")
|
66
|
+
if height:
|
67
|
+
details.append(f"height={height}")
|
68
|
+
if not preserve_aspect:
|
69
|
+
details.append("preserve_aspect=False")
|
70
|
+
info = ("; ".join(details)) if details else "auto-fit"
|
71
|
+
return tr("Image displayed: {disp_path} ({info})", disp_path=disp_path, info=info)
|
72
|
+
except Exception as e:
|
73
|
+
self.report_error(tr(" ❌ Error: {error}", error=e))
|
74
|
+
return tr("Error displaying image: {error}", error=e)
|
@@ -0,0 +1,76 @@
|
|
1
|
+
from janito.tools.tool_base import ToolBase, ToolPermissions
|
2
|
+
from janito.report_events import ReportAction
|
3
|
+
from janito.tools.adapters.local.adapter import register_local_tool
|
4
|
+
from janito.i18n import tr
|
5
|
+
from janito.tools.loop_protection_decorator import protect_against_loops
|
6
|
+
from typing import Sequence
|
7
|
+
|
8
|
+
|
9
|
+
@register_local_tool
|
10
|
+
class ShowImageGridTool(ToolBase):
|
11
|
+
"""Display multiple images in a grid inline in the terminal using rich.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
paths (list[str]): List of image file paths.
|
15
|
+
columns (int, optional): Number of columns in the grid. Default: 2.
|
16
|
+
width (int, optional): Max width for each image cell. Default: None (auto).
|
17
|
+
height (int, optional): Max height for each image cell. Default: None (auto).
|
18
|
+
preserve_aspect (bool, optional): Preserve aspect ratio. Default: True.
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
str: Status string summarizing the grid display.
|
22
|
+
"""
|
23
|
+
|
24
|
+
permissions = ToolPermissions(read=True)
|
25
|
+
tool_name = "show_image_grid"
|
26
|
+
|
27
|
+
@protect_against_loops(max_calls=5, time_window=10.0, key_field="paths")
|
28
|
+
def run(
|
29
|
+
self,
|
30
|
+
paths: Sequence[str],
|
31
|
+
columns: int = 2,
|
32
|
+
width: int | None = None,
|
33
|
+
height: int | None = None,
|
34
|
+
preserve_aspect: bool = True,
|
35
|
+
) -> str:
|
36
|
+
from janito.tools.path_utils import expand_path
|
37
|
+
from janito.tools.tool_utils import display_path
|
38
|
+
import os
|
39
|
+
|
40
|
+
try:
|
41
|
+
from rich.console import Console
|
42
|
+
from rich.columns import Columns
|
43
|
+
from PIL import Image as PILImage
|
44
|
+
from rich.panel import Panel
|
45
|
+
except Exception as e:
|
46
|
+
msg = tr("⚠️ Missing dependency: PIL/Pillow ({error})", error=e)
|
47
|
+
self.report_error(msg)
|
48
|
+
return msg
|
49
|
+
|
50
|
+
if not paths:
|
51
|
+
return tr("No images provided")
|
52
|
+
|
53
|
+
self.report_action(tr("🖼️ Show image grid ({n} images)", n=len(paths)), ReportAction.READ)
|
54
|
+
|
55
|
+
console = Console()
|
56
|
+
images = []
|
57
|
+
shown = 0
|
58
|
+
for p in paths:
|
59
|
+
fp = expand_path(p)
|
60
|
+
if not os.path.exists(fp):
|
61
|
+
self.report_warning(tr("❗ not found: {p}", p=display_path(fp)))
|
62
|
+
continue
|
63
|
+
try:
|
64
|
+
img = PILImage.open(fp)
|
65
|
+
title = f"{display_path(fp)} ({img.width}x{img.height})"
|
66
|
+
images.append(Panel.fit(title, title=display_path(fp), border_style="dim"))
|
67
|
+
shown += 1
|
68
|
+
except Exception as e:
|
69
|
+
self.report_warning(tr("⚠️ Skipped {p}: {e}", p=display_path(fp), e=e))
|
70
|
+
|
71
|
+
if not images:
|
72
|
+
return tr("No images could be displayed")
|
73
|
+
|
74
|
+
console.print(Columns(images, equal=True, expand=True, columns=columns))
|
75
|
+
self.report_success(tr("✅ Displayed {n} images", n=shown))
|
76
|
+
return tr("Displayed {shown}/{total} images in a {cols}x? grid", shown=shown, total=len(paths), cols=columns)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
"""
|
2
|
+
Function-to-Tool adapter for core plugins.
|
3
|
+
|
4
|
+
This module provides a way to wrap function-based tools into proper ToolBase classes.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import inspect
|
8
|
+
from typing import Any, Dict, List, Optional, get_type_hints
|
9
|
+
from janito.tools.tool_base import ToolBase, ToolPermissions
|
10
|
+
|
11
|
+
|
12
|
+
class FunctionToolAdapter(ToolBase):
|
13
|
+
"""Adapter that wraps a function into a ToolBase class."""
|
14
|
+
|
15
|
+
def __init__(self, func, tool_name: str = None, description: str = None):
|
16
|
+
super().__init__()
|
17
|
+
self._func = func
|
18
|
+
self.tool_name = tool_name or func.__name__
|
19
|
+
self._description = description or func.__doc__ or f"Tool: {self.tool_name}"
|
20
|
+
self.permissions = ToolPermissions(read=True, write=True, execute=True)
|
21
|
+
|
22
|
+
def run(self, **kwargs) -> Any:
|
23
|
+
"""Execute the wrapped function."""
|
24
|
+
return self._func(**kwargs)
|
25
|
+
|
26
|
+
def get_signature(self) -> Dict[str, Any]:
|
27
|
+
"""Get function signature for documentation."""
|
28
|
+
sig = inspect.signature(self._func)
|
29
|
+
type_hints = get_type_hints(self._func)
|
30
|
+
|
31
|
+
params = {}
|
32
|
+
for name, param in sig.parameters.items():
|
33
|
+
param_info = {
|
34
|
+
"type": str(type_hints.get(name, Any)),
|
35
|
+
"default": param.default if param.default != inspect.Parameter.empty else None,
|
36
|
+
"required": param.default == inspect.Parameter.empty,
|
37
|
+
}
|
38
|
+
params[name] = param_info
|
39
|
+
|
40
|
+
return {
|
41
|
+
"name": self.tool_name,
|
42
|
+
"description": self._description,
|
43
|
+
"parameters": params,
|
44
|
+
"return_type": str(type_hints.get("return", Any))
|
45
|
+
}
|
46
|
+
|
47
|
+
|
48
|
+
def create_function_tool(func, tool_name: str = None, description: str = None) -> type:
|
49
|
+
"""
|
50
|
+
Create a ToolBase class from a function.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
func: The function to wrap
|
54
|
+
tool_name: Optional custom tool name
|
55
|
+
description: Optional custom description
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
A ToolBase subclass that wraps the function
|
59
|
+
"""
|
60
|
+
|
61
|
+
class DynamicFunctionTool(FunctionToolAdapter):
|
62
|
+
def __init__(self):
|
63
|
+
super().__init__(func, tool_name, description)
|
64
|
+
|
65
|
+
return DynamicFunctionTool
|