janito 2.22.0__py3-none-any.whl → 2.24.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/README.md +0 -0
- janito/agent/setup_agent.py +14 -0
- janito/agent/templates/profiles/system_prompt_template_Developer_with_Python_Tools.txt.j2 +59 -11
- janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +53 -7
- janito/agent/templates/profiles/system_prompt_template_market_analyst.txt.j2 +108 -8
- janito/agent/templates/profiles/system_prompt_template_model_conversation_without_tools_or_context.txt.j2 +53 -1
- janito/cli/chat_mode/session.py +8 -1
- janito/cli/chat_mode/shell/commands/__init__.py +2 -0
- janito/cli/chat_mode/shell/commands/security/__init__.py +1 -0
- janito/cli/chat_mode/shell/commands/security/allowed_sites.py +94 -0
- janito/cli/chat_mode/shell/commands/security_command.py +51 -0
- janito/cli/chat_mode/shell/commands.bak.zip +0 -0
- janito/cli/chat_mode/shell/session.bak.zip +0 -0
- janito/cli/cli_commands/list_plugins.py +45 -0
- janito/cli/cli_commands/show_system_prompt.py +13 -40
- janito/cli/core/getters.py +4 -0
- janito/cli/core/runner.py +7 -2
- janito/cli/core/setters.py +10 -1
- janito/cli/main_cli.py +25 -3
- janito/cli/single_shot_mode/handler.py +3 -1
- janito/config_manager.py +10 -0
- janito/docs/GETTING_STARTED.md +0 -0
- janito/drivers/dashscope.bak.zip +0 -0
- janito/drivers/openai/README.md +0 -0
- janito/drivers/openai_responses.bak.zip +0 -0
- janito/llm/README.md +0 -0
- janito/mkdocs.yml +0 -0
- janito/plugins/__init__.py +17 -0
- janito/plugins/base.py +93 -0
- janito/plugins/discovery.py +160 -0
- janito/plugins/manager.py +185 -0
- janito/providers/dashscope.bak.zip +0 -0
- janito/providers/ibm/README.md +0 -0
- janito/shell.bak.zip +0 -0
- janito/tools/DOCSTRING_STANDARD.txt +0 -0
- janito/tools/README.md +0 -0
- janito/tools/adapters/local/__init__.py +2 -0
- janito/tools/adapters/local/adapter.py +55 -0
- janito/tools/adapters/local/ask_user.py +2 -0
- janito/tools/adapters/local/fetch_url.py +89 -4
- janito/tools/adapters/local/find_files.py +2 -0
- janito/tools/adapters/local/get_file_outline/core.py +2 -0
- janito/tools/adapters/local/get_file_outline/search_outline.py +2 -0
- janito/tools/adapters/local/open_html_in_browser.py +2 -0
- janito/tools/adapters/local/open_url.py +2 -0
- janito/tools/adapters/local/python_code_run.py +15 -10
- janito/tools/adapters/local/python_command_run.py +14 -9
- janito/tools/adapters/local/python_file_run.py +15 -10
- janito/tools/adapters/local/read_chart.py +252 -0
- janito/tools/adapters/local/read_files.py +2 -0
- janito/tools/adapters/local/replace_text_in_file.py +1 -1
- janito/tools/adapters/local/run_bash_command.py +18 -12
- janito/tools/adapters/local/run_powershell_command.py +15 -9
- janito/tools/adapters/local/search_text/core.py +2 -0
- janito/tools/adapters/local/validate_file_syntax/core.py +6 -0
- janito/tools/adapters/local/validate_file_syntax/jinja2_validator.py +47 -0
- janito/tools/adapters/local/view_file.py +2 -0
- janito/tools/loop_protection.py +115 -0
- janito/tools/loop_protection_decorator.py +110 -0
- janito/tools/outline_file.bak.zip +0 -0
- janito/tools/url_whitelist.py +121 -0
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/METADATA +411 -411
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/RECORD +52 -39
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/entry_points.txt +0 -0
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/licenses/LICENSE +0 -0
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/top_level.txt +0 -0
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,252 @@
|
|
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.tools.tool_utils import display_path
|
5
|
+
from janito.i18n import tr
|
6
|
+
import json
|
7
|
+
import os
|
8
|
+
from janito.tools.loop_protection_decorator import protect_against_loops
|
9
|
+
|
10
|
+
|
11
|
+
@register_local_tool
|
12
|
+
class ReadChartTool(ToolBase):
|
13
|
+
"""
|
14
|
+
Display charts and data visualizations in the terminal using rich.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
data (dict): Chart data in JSON format. Should contain 'type' (bar, line, pie, table) and 'data' keys.
|
18
|
+
title (str, optional): Chart title. Defaults to "Chart".
|
19
|
+
width (int, optional): Chart width. Defaults to 80.
|
20
|
+
height (int, optional): Chart height. Defaults to 20.
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
str: Formatted chart display in terminal or error message.
|
24
|
+
"""
|
25
|
+
|
26
|
+
permissions = ToolPermissions(read=True)
|
27
|
+
tool_name = "read_chart"
|
28
|
+
|
29
|
+
@protect_against_loops(max_calls=5, time_window=10.0)
|
30
|
+
def run(self, data: dict, title: str = "Chart", width: int = 80, height: int = 20) -> str:
|
31
|
+
try:
|
32
|
+
from rich.console import Console
|
33
|
+
from rich.table import Table
|
34
|
+
from rich.text import Text
|
35
|
+
from rich.layout import Layout
|
36
|
+
from rich.panel import Panel
|
37
|
+
from rich.columns import Columns
|
38
|
+
from rich import box
|
39
|
+
|
40
|
+
console = Console(width=width)
|
41
|
+
|
42
|
+
if not isinstance(data, dict):
|
43
|
+
return "❌ Error: Data must be a dictionary"
|
44
|
+
|
45
|
+
chart_type = data.get('type', 'table').lower()
|
46
|
+
chart_data = data.get('data', [])
|
47
|
+
|
48
|
+
if not chart_data:
|
49
|
+
return "⚠️ Warning: No data provided for chart"
|
50
|
+
|
51
|
+
self.report_action(
|
52
|
+
tr("📊 Displaying {chart_type} chart: {title}",
|
53
|
+
chart_type=chart_type, title=title),
|
54
|
+
ReportAction.READ
|
55
|
+
)
|
56
|
+
|
57
|
+
if chart_type == 'table':
|
58
|
+
return self._display_table(console, chart_data, title, width)
|
59
|
+
elif chart_type == 'bar':
|
60
|
+
return self._display_bar(console, chart_data, title, width, height)
|
61
|
+
elif chart_type == 'line':
|
62
|
+
return self._display_line(console, chart_data, title, width, height)
|
63
|
+
elif chart_type == 'pie':
|
64
|
+
return self._display_pie(console, chart_data, title, width)
|
65
|
+
else:
|
66
|
+
return f"❌ Error: Unsupported chart type '{chart_type}'. Use: table, bar, line, pie"
|
67
|
+
|
68
|
+
except ImportError:
|
69
|
+
return "❌ Error: rich library not available for chart display"
|
70
|
+
except Exception as e:
|
71
|
+
return f"❌ Error displaying chart: {e}"
|
72
|
+
|
73
|
+
def _display_table(self, console, data, title, width):
|
74
|
+
"""Display data as a rich table."""
|
75
|
+
from rich.table import Table
|
76
|
+
|
77
|
+
if not data:
|
78
|
+
return "No data to display"
|
79
|
+
|
80
|
+
table = Table(title=title, show_header=True, header_style="bold magenta")
|
81
|
+
|
82
|
+
# Handle different data formats
|
83
|
+
if isinstance(data, dict):
|
84
|
+
# Dictionary format: key-value pairs
|
85
|
+
table.add_column("Key", style="cyan")
|
86
|
+
table.add_column("Value", style="green")
|
87
|
+
for key, value in data.items():
|
88
|
+
table.add_row(str(key), str(value))
|
89
|
+
elif isinstance(data, list):
|
90
|
+
if data and isinstance(data[0], dict):
|
91
|
+
# List of dictionaries (records)
|
92
|
+
headers = list(data[0].keys()) if data else []
|
93
|
+
for header in headers:
|
94
|
+
table.add_column(str(header).title(), style="cyan")
|
95
|
+
for row in data:
|
96
|
+
table.add_row(*[str(row.get(h, "")) for h in headers])
|
97
|
+
else:
|
98
|
+
# Simple list
|
99
|
+
table.add_column("Items", style="cyan")
|
100
|
+
for item in data:
|
101
|
+
table.add_row(str(item))
|
102
|
+
|
103
|
+
console.print(table)
|
104
|
+
return f"✅ Table chart displayed: {title}"
|
105
|
+
|
106
|
+
def _display_bar(self, console, data, title, width, height):
|
107
|
+
"""Display data as a simple bar chart using unicode blocks."""
|
108
|
+
try:
|
109
|
+
if isinstance(data, dict):
|
110
|
+
items = list(data.items())
|
111
|
+
elif isinstance(data, list) and data and isinstance(data[0], dict):
|
112
|
+
# Assume first two keys are labels and values
|
113
|
+
keys = list(data[0].keys())
|
114
|
+
if len(keys) >= 2:
|
115
|
+
label_key, value_key = keys[0], keys[1]
|
116
|
+
items = [(item[label_key], item[value_key]) for item in data]
|
117
|
+
else:
|
118
|
+
items = [(str(i), v) for i, v in enumerate(data)]
|
119
|
+
else:
|
120
|
+
items = [(str(i), v) for i, v in enumerate(data)]
|
121
|
+
|
122
|
+
if not items:
|
123
|
+
return "No data to display"
|
124
|
+
|
125
|
+
# Convert values to numbers
|
126
|
+
numeric_items = []
|
127
|
+
for label, value in items:
|
128
|
+
try:
|
129
|
+
numeric_items.append((str(label), float(value)))
|
130
|
+
except (ValueError, TypeError):
|
131
|
+
numeric_items.append((str(label), 0.0))
|
132
|
+
|
133
|
+
if not numeric_items:
|
134
|
+
return "No valid numeric data to display"
|
135
|
+
|
136
|
+
max_val = max(val for _, val in numeric_items) if numeric_items else 1
|
137
|
+
|
138
|
+
console.print(f"\n[bold]{title}[/bold]")
|
139
|
+
console.print("=" * min(len(title), width))
|
140
|
+
|
141
|
+
for label, value in numeric_items:
|
142
|
+
bar_length = int((value / max_val) * (width - 20)) if max_val > 0 else 0
|
143
|
+
bar = "█" * bar_length
|
144
|
+
console.print(f"{label:<15} {bar} {value:.1f}")
|
145
|
+
|
146
|
+
return f"✅ Bar chart displayed: {title}"
|
147
|
+
|
148
|
+
except Exception as e:
|
149
|
+
return f"❌ Error displaying bar chart: {e}"
|
150
|
+
|
151
|
+
def _display_line(self, console, data, title, width, height):
|
152
|
+
"""Display data as a simple line chart using unicode characters."""
|
153
|
+
try:
|
154
|
+
if isinstance(data, dict):
|
155
|
+
items = list(data.items())
|
156
|
+
elif isinstance(data, list):
|
157
|
+
if data and isinstance(data[0], dict):
|
158
|
+
keys = list(data[0].keys())
|
159
|
+
if len(keys) >= 2:
|
160
|
+
label_key, value_key = keys[0], keys[1]
|
161
|
+
items = [(item[label_key], item[value_key]) for item in data]
|
162
|
+
else:
|
163
|
+
items = [(str(i), v) for i, v in enumerate(data)]
|
164
|
+
else:
|
165
|
+
items = [(str(i), v) for i, v in enumerate(data)]
|
166
|
+
else:
|
167
|
+
return "Unsupported data format"
|
168
|
+
|
169
|
+
# Convert to numeric values
|
170
|
+
points = []
|
171
|
+
for x, y in items:
|
172
|
+
try:
|
173
|
+
points.append((float(x), float(y)))
|
174
|
+
except (ValueError, TypeError):
|
175
|
+
continue
|
176
|
+
|
177
|
+
if len(points) < 2:
|
178
|
+
return "Need at least 2 data points for line chart"
|
179
|
+
|
180
|
+
points.sort(key=lambda p: p[0])
|
181
|
+
|
182
|
+
# Simple ASCII line chart
|
183
|
+
min_x, max_x = min(p[0] for p in points), max(p[0] for p in points)
|
184
|
+
min_y, max_y = min(p[1] for p in points), max(p[1] for p in points)
|
185
|
+
|
186
|
+
if max_x == min_x or max_y == min_y:
|
187
|
+
return "Cannot display line chart: all values are the same"
|
188
|
+
|
189
|
+
console.print(f"\n[bold]{title}[/bold]")
|
190
|
+
console.print("=" * min(len(title), width))
|
191
|
+
|
192
|
+
# Simple representation
|
193
|
+
for x, y in points:
|
194
|
+
x_norm = int(((x - min_x) / (max_x - min_x)) * (width - 20))
|
195
|
+
y_norm = int(((y - min_y) / (max_y - min_y)) * 10)
|
196
|
+
line = " " * x_norm + "●" + " " * (width - 20 - x_norm)
|
197
|
+
console.print(f"{x:>8.1f}: {line} {y:.1f}")
|
198
|
+
|
199
|
+
return f"✅ Line chart displayed: {title}"
|
200
|
+
|
201
|
+
except Exception as e:
|
202
|
+
return f"❌ Error displaying line chart: {e}"
|
203
|
+
|
204
|
+
def _display_pie(self, console, data, title, width):
|
205
|
+
"""Display data as a simple pie chart representation."""
|
206
|
+
try:
|
207
|
+
if isinstance(data, dict):
|
208
|
+
items = list(data.items())
|
209
|
+
elif isinstance(data, list) and data and isinstance(data[0], dict):
|
210
|
+
keys = list(data[0].keys())
|
211
|
+
if len(keys) >= 2:
|
212
|
+
label_key, value_key = keys[0], keys[1]
|
213
|
+
items = [(item[label_key], item[value_key]) for item in data]
|
214
|
+
else:
|
215
|
+
items = [(str(i), v) for i, v in enumerate(data)]
|
216
|
+
else:
|
217
|
+
items = [(str(i), v) for i, v in enumerate(data)]
|
218
|
+
|
219
|
+
# Convert to numeric values
|
220
|
+
values = []
|
221
|
+
for label, value in items:
|
222
|
+
try:
|
223
|
+
values.append((str(label), float(value)))
|
224
|
+
except (ValueError, TypeError):
|
225
|
+
continue
|
226
|
+
|
227
|
+
if not values:
|
228
|
+
return "No valid numeric data to display"
|
229
|
+
|
230
|
+
total = sum(val for _, val in values)
|
231
|
+
if total == 0:
|
232
|
+
return "Cannot display pie chart: total is zero"
|
233
|
+
|
234
|
+
console.print(f"\n[bold]{title}[/bold]")
|
235
|
+
console.print("=" * min(len(title), width))
|
236
|
+
|
237
|
+
# Unicode pie chart segments
|
238
|
+
segments = ["🟦", "🟥", "🟩", "🟨", "🟪", "🟧", "⬛", "⬜"]
|
239
|
+
|
240
|
+
for i, (label, value) in enumerate(values):
|
241
|
+
percentage = (value / total) * 100
|
242
|
+
segment = segments[i % len(segments)]
|
243
|
+
bar_length = int((value / total) * (width - 30))
|
244
|
+
bar = "█" * bar_length
|
245
|
+
console.print(f"{segment} {label:<15} {bar} {percentage:5.1f}% ({value})")
|
246
|
+
|
247
|
+
console.print(f"\n[dim]Total: {total}[/dim]")
|
248
|
+
|
249
|
+
return f"✅ Pie chart displayed: {title}"
|
250
|
+
|
251
|
+
except Exception as e:
|
252
|
+
return f"❌ Error displaying pie chart: {e}"
|
@@ -3,6 +3,7 @@ from janito.report_events import ReportAction
|
|
3
3
|
from janito.tools.adapters.local.adapter import register_local_tool
|
4
4
|
from janito.tools.tool_utils import pluralize
|
5
5
|
from janito.i18n import tr
|
6
|
+
from janito.tools.loop_protection_decorator import protect_against_loops
|
6
7
|
|
7
8
|
|
8
9
|
@register_local_tool
|
@@ -20,6 +21,7 @@ class ReadFilesTool(ToolBase):
|
|
20
21
|
permissions = ToolPermissions(read=True)
|
21
22
|
tool_name = "read_files"
|
22
23
|
|
24
|
+
@protect_against_loops(max_calls=5, time_window=10.0)
|
23
25
|
def run(self, paths: list[str]) -> str:
|
24
26
|
from janito.tools.tool_utils import display_path
|
25
27
|
import os
|
@@ -90,7 +90,7 @@ class ReplaceTextInFileTool(ToolBase):
|
|
90
90
|
f"\n{validation_result}" if validation_result else ""
|
91
91
|
)
|
92
92
|
except Exception as e:
|
93
|
-
self.report_error(tr(" ❌ Error"), ReportAction.
|
93
|
+
self.report_error(tr(" ❌ Error"), ReportAction.UPDATE)
|
94
94
|
return tr("Error replacing text: {error}", error=e)
|
95
95
|
|
96
96
|
def _read_file_content(self, path):
|
@@ -20,6 +20,7 @@ class RunBashCommandTool(ToolBase):
|
|
20
20
|
timeout (int): Timeout in seconds for the command. Defaults to 60.
|
21
21
|
require_confirmation (bool): If True, require user confirmation before running. Defaults to False.
|
22
22
|
requires_user_input (bool): If True, warns that the command may require user input and might hang. Defaults to False. Non-interactive commands are preferred for automation and reliability.
|
23
|
+
silent (bool): If True, suppresses progress and status messages. Defaults to False.
|
23
24
|
|
24
25
|
Returns:
|
25
26
|
str: File paths and line counts for stdout and stderr.
|
@@ -44,15 +45,19 @@ class RunBashCommandTool(ToolBase):
|
|
44
45
|
timeout: int = 60,
|
45
46
|
require_confirmation: bool = False,
|
46
47
|
requires_user_input: bool = False,
|
48
|
+
silent: bool = False,
|
47
49
|
) -> str:
|
48
50
|
if not command.strip():
|
49
51
|
self.report_warning(tr("ℹ️ Empty command provided."), ReportAction.EXECUTE)
|
50
52
|
return tr("Warning: Empty command provided. Operation skipped.")
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
if not silent:
|
54
|
+
self.report_action(
|
55
|
+
tr("🖥️ Run bash command: {command} ...\n", command=command),
|
56
|
+
ReportAction.EXECUTE,
|
57
|
+
)
|
58
|
+
else:
|
59
|
+
self.report_action(tr("⚡ Executing..."), ReportAction.EXECUTE)
|
60
|
+
if requires_user_input and not silent:
|
56
61
|
self.report_warning(
|
57
62
|
tr(
|
58
63
|
"⚠️ Warning: This command might be interactive, require user input, and might hang."
|
@@ -123,13 +128,14 @@ class RunBashCommandTool(ToolBase):
|
|
123
128
|
stderr_thread.join()
|
124
129
|
stdout_file.flush()
|
125
130
|
stderr_file.flush()
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
131
|
+
if not silent:
|
132
|
+
self.report_success(
|
133
|
+
tr(
|
134
|
+
" ✅ return code {return_code}",
|
135
|
+
return_code=return_code,
|
136
|
+
),
|
137
|
+
ReportAction.EXECUTE,
|
138
|
+
)
|
133
139
|
max_lines = 100
|
134
140
|
# Read back the output for summary
|
135
141
|
stdout_file.seek(0)
|
@@ -22,6 +22,7 @@ class RunPowershellCommandTool(ToolBase):
|
|
22
22
|
timeout (int): Timeout in seconds for the command. Defaults to 60.
|
23
23
|
require_confirmation (bool): If True, require user confirmation before running. Defaults to False.
|
24
24
|
requires_user_input (bool): If True, warns that the command may require user input and might hang. Defaults to False. Non-interactive commands are preferred for automation and reliability.
|
25
|
+
silent (bool): If True, suppresses progress and status messages. Defaults to False.
|
25
26
|
|
26
27
|
Returns:
|
27
28
|
str: Output and status message, or file paths/line counts if output is large.
|
@@ -41,7 +42,7 @@ class RunPowershellCommandTool(ToolBase):
|
|
41
42
|
if require_confirmation:
|
42
43
|
self.report_warning(
|
43
44
|
tr("⚠️ Confirmation requested, but no handler (auto-confirmed)."),
|
44
|
-
ReportAction.
|
45
|
+
ReportAction.EXECUTE
|
45
46
|
)
|
46
47
|
return True # Auto-confirm for now
|
47
48
|
return True
|
@@ -127,16 +128,20 @@ class RunPowershellCommandTool(ToolBase):
|
|
127
128
|
timeout: int = 60,
|
128
129
|
require_confirmation: bool = False,
|
129
130
|
requires_user_input: bool = False,
|
131
|
+
silent: bool = False,
|
130
132
|
) -> str:
|
131
133
|
if not command.strip():
|
132
134
|
self.report_warning(tr("ℹ️ Empty command provided."), ReportAction.EXECUTE)
|
133
135
|
return tr("Warning: Empty command provided. Operation skipped.")
|
134
136
|
encoding_prefix = "$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; "
|
135
137
|
command_with_encoding = encoding_prefix + command
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
138
|
+
if not silent:
|
139
|
+
self.report_action(
|
140
|
+
tr("🖥️ Running PowerShell command: {command} ...\n", command=command),
|
141
|
+
ReportAction.EXECUTE,
|
142
|
+
)
|
143
|
+
else:
|
144
|
+
self.report_action(tr("⚡ Executing..."), ReportAction.EXECUTE)
|
140
145
|
self._confirm_and_warn(command, require_confirmation, requires_user_input)
|
141
146
|
from janito.platform_discovery import PlatformDiscovery
|
142
147
|
|
@@ -199,10 +204,11 @@ class RunPowershellCommandTool(ToolBase):
|
|
199
204
|
stderr_thread.join()
|
200
205
|
stdout_file.flush()
|
201
206
|
stderr_file.flush()
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
207
|
+
if not silent:
|
208
|
+
self.report_success(
|
209
|
+
tr(" ✅ return code {return_code}", return_code=return_code),
|
210
|
+
ReportAction.EXECUTE,
|
211
|
+
)
|
206
212
|
return self._format_result(
|
207
213
|
requires_user_input, return_code, stdout_file, stderr_file
|
208
214
|
)
|
@@ -7,6 +7,7 @@ import os
|
|
7
7
|
from .pattern_utils import prepare_pattern, format_result, summarize_total
|
8
8
|
from .match_lines import read_file_lines
|
9
9
|
from .traverse_directory import traverse_directory
|
10
|
+
from janito.tools.loop_protection_decorator import protect_against_loops
|
10
11
|
|
11
12
|
|
12
13
|
from janito.tools.adapters.local.adapter import register_local_tool as register_tool
|
@@ -152,6 +153,7 @@ class SearchTextTool(ToolBase):
|
|
152
153
|
)
|
153
154
|
return info_str, dir_output, dir_limit_reached, per_file_counts
|
154
155
|
|
156
|
+
@protect_against_loops(max_calls=5, time_window=10.0)
|
155
157
|
def run(
|
156
158
|
self,
|
157
159
|
paths: str,
|
@@ -15,6 +15,8 @@ from .html_validator import validate_html
|
|
15
15
|
from .markdown_validator import validate_markdown
|
16
16
|
from .js_validator import validate_js
|
17
17
|
from .css_validator import validate_css
|
18
|
+
from .jinja2_validator import validate_jinja2
|
19
|
+
from janito.tools.loop_protection_decorator import protect_against_loops
|
18
20
|
|
19
21
|
|
20
22
|
def _get_validator(ext):
|
@@ -32,6 +34,8 @@ def _get_validator(ext):
|
|
32
34
|
".md": validate_markdown,
|
33
35
|
".js": validate_js,
|
34
36
|
".css": validate_css,
|
37
|
+
".j2": validate_jinja2,
|
38
|
+
".jinja2": validate_jinja2,
|
35
39
|
}
|
36
40
|
return mapping.get(ext)
|
37
41
|
|
@@ -73,6 +77,7 @@ class ValidateFileSyntaxTool(ToolBase):
|
|
73
77
|
- HTML (.html, .htm) [lxml]
|
74
78
|
- Markdown (.md)
|
75
79
|
- JavaScript (.js)
|
80
|
+
- Jinja2 templates (.j2, .jinja2)
|
76
81
|
|
77
82
|
Args:
|
78
83
|
path (str): Path to the file to validate.
|
@@ -86,6 +91,7 @@ class ValidateFileSyntaxTool(ToolBase):
|
|
86
91
|
permissions = ToolPermissions(read=True)
|
87
92
|
tool_name = "validate_file_syntax"
|
88
93
|
|
94
|
+
@protect_against_loops(max_calls=5, time_window=10.0)
|
89
95
|
def run(self, path: str) -> str:
|
90
96
|
disp_path = display_path(path)
|
91
97
|
self.report_action(
|
@@ -0,0 +1,47 @@
|
|
1
|
+
"""Jinja2 template syntax validator."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
from janito.i18n import tr
|
5
|
+
|
6
|
+
|
7
|
+
def validate_jinja2(path: str) -> str:
|
8
|
+
"""Validate Jinja2 template syntax."""
|
9
|
+
try:
|
10
|
+
from jinja2 import Environment, TemplateSyntaxError
|
11
|
+
|
12
|
+
with open(path, "r", encoding="utf-8") as f:
|
13
|
+
content = f.read()
|
14
|
+
|
15
|
+
# Create a Jinja2 environment and try to parse the template
|
16
|
+
env = Environment()
|
17
|
+
try:
|
18
|
+
env.parse(content)
|
19
|
+
return tr("✅ Syntax OK")
|
20
|
+
except TemplateSyntaxError as e:
|
21
|
+
line_num = getattr(e, 'lineno', 0)
|
22
|
+
return tr("⚠️ Warning: Syntax error: {error} at line {line}",
|
23
|
+
error=str(e), line=line_num)
|
24
|
+
except Exception as e:
|
25
|
+
return tr("⚠️ Warning: Syntax error: {error}", error=str(e))
|
26
|
+
|
27
|
+
except ImportError:
|
28
|
+
# If jinja2 is not available, just check basic structure
|
29
|
+
try:
|
30
|
+
with open(path, "r", encoding="utf-8") as f:
|
31
|
+
content = f.read()
|
32
|
+
|
33
|
+
# Basic checks for common Jinja2 syntax issues
|
34
|
+
open_tags = content.count("{%")
|
35
|
+
close_tags = content.count("%}")
|
36
|
+
open_vars = content.count("{{")
|
37
|
+
close_vars = content.count("}}")
|
38
|
+
|
39
|
+
if open_tags != close_tags:
|
40
|
+
return tr("⚠️ Warning: Syntax error: Mismatched Jinja2 tags")
|
41
|
+
if open_vars != close_vars:
|
42
|
+
return tr("⚠️ Warning: Syntax error: Mismatched Jinja2 variables")
|
43
|
+
|
44
|
+
return tr("✅ Syntax OK (basic validation)")
|
45
|
+
|
46
|
+
except Exception as e:
|
47
|
+
return tr("⚠️ Warning: Syntax error: {error}", error=str(e))
|
@@ -3,6 +3,7 @@ from janito.report_events import ReportAction
|
|
3
3
|
from janito.tools.adapters.local.adapter import register_local_tool
|
4
4
|
from janito.tools.tool_utils import pluralize
|
5
5
|
from janito.i18n import tr
|
6
|
+
from janito.tools.loop_protection_decorator import protect_against_loops
|
6
7
|
|
7
8
|
|
8
9
|
@register_local_tool
|
@@ -28,6 +29,7 @@ class ViewFileTool(ToolBase):
|
|
28
29
|
permissions = ToolPermissions(read=True)
|
29
30
|
tool_name = "view_file"
|
30
31
|
|
32
|
+
@protect_against_loops(max_calls=5, time_window=10.0)
|
31
33
|
def run(self, path: str, from_line: int = None, to_line: int = None) -> str:
|
32
34
|
import os
|
33
35
|
from janito.tools.tool_utils import display_path
|
@@ -0,0 +1,115 @@
|
|
1
|
+
import time
|
2
|
+
import threading
|
3
|
+
from typing import Dict, List, Tuple
|
4
|
+
from janito.tools.tool_use_tracker import normalize_path
|
5
|
+
|
6
|
+
|
7
|
+
class LoopProtection:
|
8
|
+
"""
|
9
|
+
Provides loop protection for tool calls by tracking repeated operations
|
10
|
+
on the same resources within a short time period.
|
11
|
+
|
12
|
+
This class monitors file operations and prevents excessive reads on the same
|
13
|
+
file within a configurable time window. It helps prevent infinite loops or
|
14
|
+
excessive resource consumption when tools repeatedly access the same files.
|
15
|
+
|
16
|
+
The default configuration allows up to 5 operations on the same file within
|
17
|
+
a 10-second window. Operations outside this window are automatically cleaned
|
18
|
+
up to prevent memory accumulation.
|
19
|
+
"""
|
20
|
+
|
21
|
+
_instance = None
|
22
|
+
_lock = threading.Lock()
|
23
|
+
|
24
|
+
def __new__(cls):
|
25
|
+
if not cls._instance:
|
26
|
+
with cls._lock:
|
27
|
+
if not cls._instance:
|
28
|
+
cls._instance = super().__new__(cls)
|
29
|
+
cls._instance._init_protection()
|
30
|
+
return cls._instance
|
31
|
+
|
32
|
+
def _init_protection(self):
|
33
|
+
# Track file operations: {normalized_path: [(timestamp, operation_type), ...]}
|
34
|
+
self._file_operations: Dict[str, List[Tuple[float, str]]] = {}
|
35
|
+
# Time window for detecting loops (in seconds)
|
36
|
+
self._time_window = 10.0
|
37
|
+
# Maximum allowed operations on the same file within time window
|
38
|
+
self._max_operations = 5
|
39
|
+
|
40
|
+
"""
|
41
|
+
Configuration parameters:
|
42
|
+
|
43
|
+
_time_window: Time window in seconds for detecting excessive operations.
|
44
|
+
Default is 10.0 seconds.
|
45
|
+
|
46
|
+
_max_operations: Maximum number of operations allowed on the same file
|
47
|
+
within the time window. Default is 5 operations.
|
48
|
+
"""
|
49
|
+
|
50
|
+
def check_file_operation_limit(self, path: str, operation_type: str) -> bool:
|
51
|
+
"""
|
52
|
+
Check if performing an operation on a file would exceed the limit.
|
53
|
+
|
54
|
+
This method tracks file operations and prevents excessive reads on the same
|
55
|
+
file within a configurable time window (default 10 seconds). It helps prevent
|
56
|
+
infinite loops or excessive resource consumption when tools repeatedly access
|
57
|
+
the same files.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
path: The file path being operated on
|
61
|
+
operation_type: Type of operation (e.g., "view_file", "read_files")
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
bool: True if operation is allowed, False if it would exceed the limit
|
65
|
+
|
66
|
+
Example:
|
67
|
+
>>> loop_protection = LoopProtection.instance()
|
68
|
+
>>> if loop_protection.check_file_operation_limit("/path/to/file.txt", "view_file"):
|
69
|
+
... # Safe to proceed with file operation
|
70
|
+
... content = read_file("/path/to/file.txt")
|
71
|
+
... else:
|
72
|
+
... # Would exceed limit - potential loop detected
|
73
|
+
... raise RuntimeError("Too many operations on the same file")
|
74
|
+
"""
|
75
|
+
norm_path = normalize_path(path)
|
76
|
+
current_time = time.time()
|
77
|
+
|
78
|
+
# Clean up old operations outside the time window
|
79
|
+
if norm_path in self._file_operations:
|
80
|
+
self._file_operations[norm_path] = [
|
81
|
+
(timestamp, op_type)
|
82
|
+
for timestamp, op_type in self._file_operations[norm_path]
|
83
|
+
if current_time - timestamp <= self._time_window
|
84
|
+
]
|
85
|
+
|
86
|
+
# Check if we're exceeding the limit
|
87
|
+
if norm_path in self._file_operations:
|
88
|
+
operations = self._file_operations[norm_path]
|
89
|
+
if len(operations) >= self._max_operations:
|
90
|
+
# Check if all recent operations are within the time window
|
91
|
+
if all(current_time - timestamp <= self._time_window
|
92
|
+
for timestamp, _ in operations):
|
93
|
+
return False # Would exceed limit - potential loop
|
94
|
+
|
95
|
+
# Record this operation
|
96
|
+
if norm_path not in self._file_operations:
|
97
|
+
self._file_operations[norm_path] = []
|
98
|
+
self._file_operations[norm_path].append((current_time, operation_type))
|
99
|
+
|
100
|
+
return True # Operation allowed
|
101
|
+
|
102
|
+
def reset_tracking(self):
|
103
|
+
"""
|
104
|
+
Reset all tracking data.
|
105
|
+
|
106
|
+
This method clears all recorded file operations, effectively resetting
|
107
|
+
the loop protection state. This can be useful in testing scenarios or
|
108
|
+
when you want to explicitly clear the tracking history.
|
109
|
+
"""
|
110
|
+
with self._lock:
|
111
|
+
self._file_operations.clear()
|
112
|
+
|
113
|
+
@classmethod
|
114
|
+
def instance(cls):
|
115
|
+
return cls()
|