janito 3.3.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.
- janito/README.md +3 -0
- janito/cli/chat_mode/bindings.py +50 -0
- janito/cli/chat_mode/session.py +12 -1
- janito/cli/chat_mode/shell/commands/multi.py +5 -0
- janito/cli/chat_mode/shell/commands/security/allowed_sites.py +47 -33
- janito/cli/cli_commands/list_plugins.py +13 -8
- janito/cli/core/model_guesser.py +40 -24
- janito/cli/prompt_core.py +47 -9
- janito/cli/rich_terminal_reporter.py +2 -2
- janito/hello.txt +0 -0
- janito/i18n/it.py +46 -46
- janito/llm/agent.py +32 -16
- janito/llm/cancellation_manager.py +63 -0
- janito/llm/driver.py +8 -0
- janito/llm/enter_cancellation.py +107 -0
- janito/{plugin_system_backup_20250825_070018 → plugin_system}/core_loader.py +76 -3
- janito/{plugin_system_backup_20250825_070018 → plugin_system}/core_loader_fixed.py +79 -3
- janito/plugins/__init__.py +29 -21
- janito/{plugins_backup_20250825_070018 → plugins}/auto_loader.py +12 -11
- janito/plugins/builtin.py +1 -1
- janito/plugins/core/filemanager/tools/copy_file.py +1 -25
- janito/plugins/core/filemanager/tools/create_directory.py +1 -28
- janito/plugins/core/filemanager/tools/create_file.py +3 -27
- janito/plugins/core/filemanager/tools/delete_text_in_file.py +0 -1
- janito/plugins/core/imagedisplay/plugin.py +1 -1
- janito/plugins/core_adapter.py +131 -0
- janito/plugins/discovery.py +3 -3
- janito/{plugins_backup_20250825_070018 → plugins}/discovery_core.py +14 -9
- janito/plugins/example_plugin.py +1 -1
- janito/plugins/manager.py +1 -1
- janito/{plugins_backup_20250825_070018 → plugins}/tools/core_tools_plugin.py +9 -10
- janito/{plugins_backup_20250825_070018 → plugins}/tools/create_file.py +2 -2
- janito/{plugins_backup_20250825_070018 → plugins}/tools/delete_text_in_file.py +0 -1
- janito/providers/__init__.py +1 -0
- janito/providers/together/__init__.py +1 -0
- janito/providers/together/model_info.py +69 -0
- janito/providers/together/provider.py +108 -0
- janito/tools/base.py +1 -31
- janito/tools/cli_initializer.py +1 -1
- janito/tools/initialize.py +1 -1
- janito/tools/loop_protection_decorator.py +114 -117
- janito/tools/tool_base.py +114 -142
- janito/tools/tools_schema.py +6 -12
- janito-3.5.0.dist-info/METADATA +229 -0
- {janito-3.3.0.dist-info → janito-3.5.0.dist-info}/RECORD +98 -162
- janito/plugins/__main__.py +0 -85
- janito/plugins/base.py +0 -57
- janito/plugins/core_loader.py +0 -144
- janito/plugins_backup_20250825_070018/__init__.py +0 -36
- janito/plugins_backup_20250825_070018/builtin.py +0 -102
- janito/plugins_backup_20250825_070018/config.py +0 -84
- janito/plugins_backup_20250825_070018/core/__init__.py +0 -7
- janito/plugins_backup_20250825_070018/core/codeanalyzer/__init__.py +0 -43
- janito/plugins_backup_20250825_070018/core/codeanalyzer/tools/get_file_outline/__init__.py +0 -1
- janito/plugins_backup_20250825_070018/core/codeanalyzer/tools/get_file_outline/core.py +0 -122
- janito/plugins_backup_20250825_070018/core/codeanalyzer/tools/search_text/__init__.py +0 -1
- janito/plugins_backup_20250825_070018/core/codeanalyzer/tools/search_text/core.py +0 -205
- janito/plugins_backup_20250825_070018/core/filemanager/__init__.py +0 -124
- janito/plugins_backup_20250825_070018/core/filemanager/tools/copy_file.py +0 -87
- janito/plugins_backup_20250825_070018/core/filemanager/tools/create_directory.py +0 -70
- janito/plugins_backup_20250825_070018/core/filemanager/tools/create_file.py +0 -87
- janito/plugins_backup_20250825_070018/core/filemanager/tools/delete_text_in_file.py +0 -135
- janito/plugins_backup_20250825_070018/core/filemanager/tools/find_files.py +0 -143
- janito/plugins_backup_20250825_070018/core/filemanager/tools/move_file.py +0 -131
- janito/plugins_backup_20250825_070018/core/filemanager/tools/read_files.py +0 -58
- janito/plugins_backup_20250825_070018/core/filemanager/tools/remove_directory.py +0 -55
- janito/plugins_backup_20250825_070018/core/filemanager/tools/remove_file.py +0 -58
- janito/plugins_backup_20250825_070018/core/filemanager/tools/replace_text_in_file.py +0 -270
- janito/plugins_backup_20250825_070018/core/filemanager/tools/validate_file_syntax/__init__.py +0 -1
- janito/plugins_backup_20250825_070018/core/filemanager/tools/validate_file_syntax/core.py +0 -114
- janito/plugins_backup_20250825_070018/core/filemanager/tools/view_file.py +0 -172
- janito/plugins_backup_20250825_070018/core/imagedisplay/__init__.py +0 -14
- janito/plugins_backup_20250825_070018/core/imagedisplay/plugin.py +0 -51
- janito/plugins_backup_20250825_070018/core/imagedisplay/tools/__init__.py +0 -1
- janito/plugins_backup_20250825_070018/core/imagedisplay/tools/show_image.py +0 -83
- janito/plugins_backup_20250825_070018/core/imagedisplay/tools/show_image_grid.py +0 -84
- janito/plugins_backup_20250825_070018/core/system/__init__.py +0 -23
- janito/plugins_backup_20250825_070018/core/system/tools/run_bash_command.py +0 -183
- janito/plugins_backup_20250825_070018/core/system/tools/run_powershell_command.py +0 -218
- janito/plugins_backup_20250825_070018/core_adapter.py +0 -55
- janito/plugins_backup_20250825_070018/dev/__init__.py +0 -7
- janito/plugins_backup_20250825_070018/dev/pythondev/__init__.py +0 -37
- janito/plugins_backup_20250825_070018/dev/pythondev/tools/python_code_run.py +0 -172
- janito/plugins_backup_20250825_070018/dev/pythondev/tools/python_command_run.py +0 -171
- janito/plugins_backup_20250825_070018/dev/pythondev/tools/python_file_run.py +0 -172
- janito/plugins_backup_20250825_070018/dev/visualization/__init__.py +0 -23
- janito/plugins_backup_20250825_070018/dev/visualization/tools/read_chart.py +0 -259
- janito/plugins_backup_20250825_070018/discovery.py +0 -289
- janito/plugins_backup_20250825_070018/example_plugin.py +0 -108
- janito/plugins_backup_20250825_070018/manager.py +0 -243
- janito/plugins_backup_20250825_070018/tools/get_file_outline/java_outline.py +0 -47
- janito/plugins_backup_20250825_070018/tools/get_file_outline/markdown_outline.py +0 -14
- janito/plugins_backup_20250825_070018/tools/get_file_outline/python_outline.py +0 -303
- janito/plugins_backup_20250825_070018/tools/get_file_outline/search_outline.py +0 -36
- janito/plugins_backup_20250825_070018/tools/search_text/match_lines.py +0 -67
- janito/plugins_backup_20250825_070018/tools/search_text/pattern_utils.py +0 -73
- janito/plugins_backup_20250825_070018/tools/search_text/traverse_directory.py +0 -145
- janito/plugins_backup_20250825_070018/tools/validate_file_syntax/css_validator.py +0 -35
- janito/plugins_backup_20250825_070018/tools/validate_file_syntax/html_validator.py +0 -100
- janito/plugins_backup_20250825_070018/tools/validate_file_syntax/jinja2_validator.py +0 -50
- janito/plugins_backup_20250825_070018/tools/validate_file_syntax/js_validator.py +0 -27
- janito/plugins_backup_20250825_070018/tools/validate_file_syntax/json_validator.py +0 -6
- janito/plugins_backup_20250825_070018/tools/validate_file_syntax/markdown_validator.py +0 -109
- janito/plugins_backup_20250825_070018/tools/validate_file_syntax/ps1_validator.py +0 -32
- janito/plugins_backup_20250825_070018/tools/validate_file_syntax/python_validator.py +0 -5
- janito/plugins_backup_20250825_070018/tools/validate_file_syntax/xml_validator.py +0 -11
- janito/plugins_backup_20250825_070018/tools/validate_file_syntax/yaml_validator.py +0 -6
- janito/plugins_backup_20250825_070018/ui/__init__.py +0 -7
- janito/plugins_backup_20250825_070018/ui/userinterface/__init__.py +0 -16
- janito/plugins_backup_20250825_070018/ui/userinterface/tools/ask_user.py +0 -110
- janito/plugins_backup_20250825_070018/web/__init__.py +0 -7
- janito/plugins_backup_20250825_070018/web/webtools/__init__.py +0 -33
- janito/plugins_backup_20250825_070018/web/webtools/tools/fetch_url.py +0 -458
- janito/plugins_backup_20250825_070018/web/webtools/tools/open_html_in_browser.py +0 -51
- janito/plugins_backup_20250825_070018/web/webtools/tools/open_url.py +0 -37
- janito/tools/function_adapter.py +0 -176
- janito-3.3.0.dist-info/METADATA +0 -83
- /janito/{plugin_system_backup_20250825_070018 → plugin_system}/__init__.py +0 -0
- /janito/{plugin_system_backup_20250825_070018 → plugin_system}/base.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/auto_loader_fixed.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/__init__.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/ask_user.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/copy_file.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/create_directory.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/decorators.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/fetch_url.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/find_files.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/get_file_outline/__init__.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/get_file_outline/core.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/codeanalyzer → plugins}/tools/get_file_outline/java_outline.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/codeanalyzer → plugins}/tools/get_file_outline/markdown_outline.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/codeanalyzer → plugins}/tools/get_file_outline/python_outline.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/codeanalyzer → plugins}/tools/get_file_outline/search_outline.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/move_file.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/open_html_in_browser.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/open_url.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/python_code_run.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/python_command_run.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/python_file_run.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/read_chart.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/read_files.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/remove_directory.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/remove_file.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/replace_text_in_file.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/run_bash_command.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/run_powershell_command.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/search_text/__init__.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/search_text/core.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/codeanalyzer → plugins}/tools/search_text/match_lines.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/codeanalyzer → plugins}/tools/search_text/pattern_utils.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/codeanalyzer → plugins}/tools/search_text/traverse_directory.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/show_image.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/show_image_grid.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/validate_file_syntax/__init__.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/validate_file_syntax/core.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/filemanager → plugins}/tools/validate_file_syntax/css_validator.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/filemanager → plugins}/tools/validate_file_syntax/html_validator.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/filemanager → plugins}/tools/validate_file_syntax/jinja2_validator.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/filemanager → plugins}/tools/validate_file_syntax/js_validator.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/filemanager → plugins}/tools/validate_file_syntax/json_validator.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/filemanager → plugins}/tools/validate_file_syntax/markdown_validator.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/filemanager → plugins}/tools/validate_file_syntax/ps1_validator.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/filemanager → plugins}/tools/validate_file_syntax/python_validator.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/filemanager → plugins}/tools/validate_file_syntax/xml_validator.py +0 -0
- /janito/{plugins_backup_20250825_070018/core/filemanager → plugins}/tools/validate_file_syntax/yaml_validator.py +0 -0
- /janito/{plugins_backup_20250825_070018 → plugins}/tools/view_file.py +0 -0
- {janito-3.3.0.dist-info → janito-3.5.0.dist-info}/WHEEL +0 -0
- {janito-3.3.0.dist-info → janito-3.5.0.dist-info}/entry_points.txt +0 -0
- {janito-3.3.0.dist-info → janito-3.5.0.dist-info}/licenses/LICENSE +0 -0
- {janito-3.3.0.dist-info → janito-3.5.0.dist-info}/top_level.txt +0 -0
@@ -1,16 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
User Interface Plugin
|
3
|
-
|
4
|
-
User interaction and input tools.
|
5
|
-
"""
|
6
|
-
|
7
|
-
|
8
|
-
def ask_user(question: str) -> str:
|
9
|
-
"""Prompt user for input/clarification"""
|
10
|
-
return f"ask_user(question='{question}')"
|
11
|
-
|
12
|
-
|
13
|
-
# Plugin metadata
|
14
|
-
__plugin_name__ = "ui.userinterface"
|
15
|
-
__plugin_description__ = "User interaction and input"
|
16
|
-
__plugin_tools__ = [ask_user]
|
@@ -1,110 +0,0 @@
|
|
1
|
-
from janito.tools.tool_base import ToolBase, ToolPermissions
|
2
|
-
from janito.tools.adapters.local.adapter import register_local_tool
|
3
|
-
from janito.tools.loop_protection_decorator import protect_against_loops
|
4
|
-
|
5
|
-
from rich import print as rich_print
|
6
|
-
from janito.i18n import tr
|
7
|
-
from rich.panel import Panel
|
8
|
-
from prompt_toolkit import PromptSession
|
9
|
-
from prompt_toolkit.key_binding import KeyBindings
|
10
|
-
from prompt_toolkit.enums import EditingMode
|
11
|
-
from prompt_toolkit.formatted_text import HTML
|
12
|
-
from janito.cli.chat_mode.prompt_style import chat_shell_style
|
13
|
-
from prompt_toolkit.styles import Style
|
14
|
-
|
15
|
-
toolbar_style = Style.from_dict({"bottom-toolbar": "fg:yellow bg:darkred"})
|
16
|
-
|
17
|
-
|
18
|
-
@register_local_tool
|
19
|
-
class AskUserTool(ToolBase):
|
20
|
-
"""
|
21
|
-
Prompts the user for clarification or input with a question.
|
22
|
-
|
23
|
-
Args:
|
24
|
-
question (str): The question to ask the user. This parameter is required and should be a string containing the prompt or question to display to the user.
|
25
|
-
Returns:
|
26
|
-
str: The user's response as a string. Example:
|
27
|
-
- "Yes"
|
28
|
-
- "No"
|
29
|
-
- "Some detailed answer..."
|
30
|
-
"""
|
31
|
-
|
32
|
-
permissions = ToolPermissions(read=True)
|
33
|
-
tool_name = "ask_user"
|
34
|
-
|
35
|
-
@protect_against_loops(max_calls=5, time_window=10.0, key_field="question")
|
36
|
-
def run(self, question: str) -> str:
|
37
|
-
|
38
|
-
print() # Print an empty line before the question panel
|
39
|
-
rich_print(Panel.fit(question, title=tr("Question"), style="cyan"))
|
40
|
-
|
41
|
-
bindings = KeyBindings()
|
42
|
-
mode = {"multiline": False}
|
43
|
-
|
44
|
-
@bindings.add("c-r")
|
45
|
-
def _(event):
|
46
|
-
pass
|
47
|
-
|
48
|
-
@bindings.add("f12")
|
49
|
-
def _(event):
|
50
|
-
buf = event.app.current_buffer
|
51
|
-
buf.text = "Do It"
|
52
|
-
buf.validate_and_handle()
|
53
|
-
|
54
|
-
@bindings.add("f2")
|
55
|
-
def _(event):
|
56
|
-
buf = event.app.current_buffer
|
57
|
-
buf.text = "F2"
|
58
|
-
buf.validate_and_handle()
|
59
|
-
|
60
|
-
# Use shared CLI styles
|
61
|
-
|
62
|
-
# prompt_style contains the prompt area and input background
|
63
|
-
# toolbar_style contains the bottom-toolbar styling
|
64
|
-
|
65
|
-
# Use the shared chat_shell_style for input styling only
|
66
|
-
style = chat_shell_style
|
67
|
-
|
68
|
-
def get_toolbar():
|
69
|
-
f12_hint = " F2: F2 | F12: Do It"
|
70
|
-
if mode["multiline"]:
|
71
|
-
return HTML(
|
72
|
-
f"<b>Multiline mode (Esc+Enter to submit). Type /single to switch.</b>{f12_hint}"
|
73
|
-
)
|
74
|
-
else:
|
75
|
-
return HTML(
|
76
|
-
f"<b>Single-line mode (Enter to submit). Type /multi for multiline.</b>{f12_hint}"
|
77
|
-
)
|
78
|
-
|
79
|
-
session = PromptSession(
|
80
|
-
multiline=False,
|
81
|
-
key_bindings=bindings,
|
82
|
-
editing_mode=EditingMode.EMACS,
|
83
|
-
bottom_toolbar=get_toolbar,
|
84
|
-
style=style,
|
85
|
-
)
|
86
|
-
|
87
|
-
prompt_icon = HTML("<inputline>💬 </inputline>")
|
88
|
-
|
89
|
-
while True:
|
90
|
-
response = session.prompt(prompt_icon)
|
91
|
-
if not mode["multiline"] and response.strip() == "/multi":
|
92
|
-
mode["multiline"] = True
|
93
|
-
session.multiline = True
|
94
|
-
continue
|
95
|
-
elif mode["multiline"] and response.strip() == "/single":
|
96
|
-
mode["multiline"] = False
|
97
|
-
session.multiline = False
|
98
|
-
continue
|
99
|
-
else:
|
100
|
-
sanitized = response.strip()
|
101
|
-
try:
|
102
|
-
sanitized.encode("utf-8")
|
103
|
-
except UnicodeEncodeError:
|
104
|
-
sanitized = sanitized.encode("utf-8", errors="replace").decode(
|
105
|
-
"utf-8"
|
106
|
-
)
|
107
|
-
rich_print(
|
108
|
-
"[yellow]Warning: Some characters in your input were not valid UTF-8 and have been replaced.[/yellow]"
|
109
|
-
)
|
110
|
-
return sanitized
|
@@ -1,33 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Web Tools Plugin
|
3
|
-
|
4
|
-
Web scraping, browsing, and URL operations.
|
5
|
-
"""
|
6
|
-
|
7
|
-
from typing import Optional, Dict, List
|
8
|
-
|
9
|
-
|
10
|
-
def fetch_url(
|
11
|
-
url: str,
|
12
|
-
search_strings: Optional[List[str]] = None,
|
13
|
-
max_length: int = 5000,
|
14
|
-
timeout: int = 10,
|
15
|
-
) -> str:
|
16
|
-
"""Download web pages with advanced options"""
|
17
|
-
return f"fetch_url(url='{url}', search={len(search_strings or [])} terms)"
|
18
|
-
|
19
|
-
|
20
|
-
def open_url(url: str) -> str:
|
21
|
-
"""Open URLs in default browser"""
|
22
|
-
return f"open_url(url='{url}')"
|
23
|
-
|
24
|
-
|
25
|
-
def open_html_in_browser(path: str) -> str:
|
26
|
-
"""Open local HTML files in browser"""
|
27
|
-
return f"open_html_in_browser(path='{path}')"
|
28
|
-
|
29
|
-
|
30
|
-
# Plugin metadata
|
31
|
-
__plugin_name__ = "web.webtools"
|
32
|
-
__plugin_description__ = "Web scraping, browsing, and URL operations"
|
33
|
-
__plugin_tools__ = [fetch_url, open_url, open_html_in_browser]
|
@@ -1,458 +0,0 @@
|
|
1
|
-
import requests
|
2
|
-
import time
|
3
|
-
import os
|
4
|
-
import json
|
5
|
-
from pathlib import Path
|
6
|
-
from bs4 import BeautifulSoup
|
7
|
-
from typing import Dict, Any, Optional
|
8
|
-
from janito.tools.adapters.local.adapter import register_local_tool
|
9
|
-
from janito.tools.tool_base import ToolBase, ToolPermissions
|
10
|
-
from janito.report_events import ReportAction
|
11
|
-
from janito.i18n import tr
|
12
|
-
from janito.tools.tool_utils import pluralize
|
13
|
-
from janito.tools.loop_protection_decorator import protect_against_loops
|
14
|
-
|
15
|
-
|
16
|
-
@register_local_tool
|
17
|
-
class FetchUrlTool(ToolBase):
|
18
|
-
"""
|
19
|
-
Fetch the content of a web page and extract its text.
|
20
|
-
|
21
|
-
This tool implements a **session-based caching mechanism** that provides
|
22
|
-
**in-memory caching** for the lifetime of the tool instance. URLs are cached
|
23
|
-
in RAM during the session, providing instant access to previously fetched
|
24
|
-
content without making additional HTTP requests.
|
25
|
-
|
26
|
-
**Session Cache Behavior:**
|
27
|
-
- **Lifetime**: Cache exists for the lifetime of the FetchUrlTool instance
|
28
|
-
- **Scope**: In-memory (RAM) cache, not persisted to disk
|
29
|
-
- **Storage**: Successful responses are cached as raw HTML content
|
30
|
-
- **Key**: Cache key is the exact URL string
|
31
|
-
- **Invalidation**: Cache is automatically cleared when the tool instance is destroyed
|
32
|
-
- **Performance**: Subsequent requests for the same URL return instantly
|
33
|
-
|
34
|
-
**Error Cache Behavior:**
|
35
|
-
- HTTP 403 errors: Cached for 24 hours (more permanent)
|
36
|
-
- HTTP 404 errors: Cached for 1 hour (temporary)
|
37
|
-
- Other 4xx errors: Cached for 30 minutes
|
38
|
-
- 5xx errors: Not cached (retried on each request)
|
39
|
-
|
40
|
-
Args:
|
41
|
-
url (str): The URL of the web page to fetch.
|
42
|
-
search_strings (list[str], optional): Strings to search for in the page content.
|
43
|
-
max_length (int, optional): Maximum number of characters to return. Defaults to 5000.
|
44
|
-
max_lines (int, optional): Maximum number of lines to return. Defaults to 200.
|
45
|
-
context_chars (int, optional): Characters of context around search matches. Defaults to 400.
|
46
|
-
timeout (int, optional): Timeout in seconds for the HTTP request. Defaults to 10.
|
47
|
-
save_to_file (str, optional): File path to save the full resource content. If provided,
|
48
|
-
the complete response will be saved to this file instead of being processed.
|
49
|
-
headers (Dict[str, str], optional): Custom HTTP headers to send with the request.
|
50
|
-
cookies (Dict[str, str], optional): Custom cookies to send with the request.
|
51
|
-
follow_redirects (bool, optional): Whether to follow HTTP redirects. Defaults to True.
|
52
|
-
Returns:
|
53
|
-
str: Extracted text content from the web page, or a warning message. Example:
|
54
|
-
- "<main text content...>"
|
55
|
-
- "No lines found for the provided search strings."
|
56
|
-
- "Warning: Empty URL provided. Operation skipped."
|
57
|
-
"""
|
58
|
-
|
59
|
-
permissions = ToolPermissions(read=True)
|
60
|
-
tool_name = "fetch_url"
|
61
|
-
|
62
|
-
def __init__(self):
|
63
|
-
super().__init__()
|
64
|
-
self.cache_dir = Path.home() / ".janito" / "cache" / "fetch_url"
|
65
|
-
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
66
|
-
self.cache_file = self.cache_dir / "error_cache.json"
|
67
|
-
self.session_cache = (
|
68
|
-
{}
|
69
|
-
) # In-memory session cache - lifetime matches tool instance
|
70
|
-
self._load_cache()
|
71
|
-
|
72
|
-
# Browser-like session with cookies and headers
|
73
|
-
self.session = requests.Session()
|
74
|
-
self.session.headers.update(
|
75
|
-
{
|
76
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
77
|
-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
78
|
-
"Accept-Language": "en-US,en;q=0.5",
|
79
|
-
"Accept-Encoding": "gzip, deflate, br",
|
80
|
-
"DNT": "1",
|
81
|
-
"Connection": "keep-alive",
|
82
|
-
"Upgrade-Insecure-Requests": "1",
|
83
|
-
}
|
84
|
-
)
|
85
|
-
|
86
|
-
# Load cookies from disk if they exist
|
87
|
-
self.cookies_file = self.cache_dir / "cookies.json"
|
88
|
-
self._load_cookies()
|
89
|
-
|
90
|
-
def _load_cache(self):
|
91
|
-
"""Load error cache from disk."""
|
92
|
-
if self.cache_file.exists():
|
93
|
-
try:
|
94
|
-
with open(self.cache_file, "r", encoding="utf-8") as f:
|
95
|
-
self.error_cache = json.load(f)
|
96
|
-
except (json.JSONDecodeError, IOError):
|
97
|
-
self.error_cache = {}
|
98
|
-
else:
|
99
|
-
self.error_cache = {}
|
100
|
-
|
101
|
-
def _save_cache(self):
|
102
|
-
"""Save error cache to disk."""
|
103
|
-
try:
|
104
|
-
with open(self.cache_file, "w", encoding="utf-8") as f:
|
105
|
-
json.dump(self.error_cache, f, indent=2)
|
106
|
-
except IOError:
|
107
|
-
pass # Silently fail if we can't write cache
|
108
|
-
|
109
|
-
def _load_cookies(self):
|
110
|
-
"""Load cookies from disk into session."""
|
111
|
-
if self.cookies_file.exists():
|
112
|
-
try:
|
113
|
-
with open(self.cookies_file, "r", encoding="utf-8") as f:
|
114
|
-
cookies_data = json.load(f)
|
115
|
-
for cookie in cookies_data:
|
116
|
-
self.session.cookies.set(**cookie)
|
117
|
-
except (json.JSONDecodeError, IOError):
|
118
|
-
pass # Silently fail if we can't load cookies
|
119
|
-
|
120
|
-
def _save_cookies(self):
|
121
|
-
"""Save session cookies to disk."""
|
122
|
-
try:
|
123
|
-
cookies_data = []
|
124
|
-
for cookie in self.session.cookies:
|
125
|
-
cookies_data.append(
|
126
|
-
{
|
127
|
-
"name": cookie.name,
|
128
|
-
"value": cookie.value,
|
129
|
-
"domain": cookie.domain,
|
130
|
-
"path": cookie.path,
|
131
|
-
}
|
132
|
-
)
|
133
|
-
with open(self.cookies_file, "w", encoding="utf-8") as f:
|
134
|
-
json.dump(cookies_data, f, indent=2)
|
135
|
-
except IOError:
|
136
|
-
pass # Silently fail if we can't write cookies
|
137
|
-
|
138
|
-
def _get_cached_error(self, url: str) -> tuple[str, bool]:
|
139
|
-
"""
|
140
|
-
Check if we have a cached error for this URL.
|
141
|
-
Returns (error_message, is_cached) tuple.
|
142
|
-
"""
|
143
|
-
if url not in self.error_cache:
|
144
|
-
return None, False
|
145
|
-
|
146
|
-
entry = self.error_cache[url]
|
147
|
-
current_time = time.time()
|
148
|
-
|
149
|
-
# Different expiration times for different status codes
|
150
|
-
if entry["status_code"] == 403:
|
151
|
-
# Cache 403 errors for 24 hours (more permanent)
|
152
|
-
expiration_time = 24 * 3600
|
153
|
-
elif entry["status_code"] == 404:
|
154
|
-
# Cache 404 errors for 1 hour (more temporary)
|
155
|
-
expiration_time = 3600
|
156
|
-
else:
|
157
|
-
# Cache other 4xx errors for 30 minutes
|
158
|
-
expiration_time = 1800
|
159
|
-
|
160
|
-
if current_time - entry["timestamp"] > expiration_time:
|
161
|
-
# Cache expired, remove it
|
162
|
-
del self.error_cache[url]
|
163
|
-
self._save_cache()
|
164
|
-
return None, False
|
165
|
-
|
166
|
-
return entry["message"], True
|
167
|
-
|
168
|
-
def _cache_error(self, url: str, status_code: int, message: str):
|
169
|
-
"""Cache an HTTP error response."""
|
170
|
-
self.error_cache[url] = {
|
171
|
-
"status_code": status_code,
|
172
|
-
"message": message,
|
173
|
-
"timestamp": time.time(),
|
174
|
-
}
|
175
|
-
self._save_cache()
|
176
|
-
|
177
|
-
def _fetch_url_content(
|
178
|
-
self,
|
179
|
-
url: str,
|
180
|
-
timeout: int = 10,
|
181
|
-
headers: Optional[Dict[str, str]] = None,
|
182
|
-
cookies: Optional[Dict[str, str]] = None,
|
183
|
-
follow_redirects: bool = True,
|
184
|
-
) -> str:
|
185
|
-
"""Fetch URL content and handle HTTP errors.
|
186
|
-
|
187
|
-
Implements two-tier caching:
|
188
|
-
1. Session cache: In-memory cache for successful responses (lifetime = tool instance)
|
189
|
-
2. Error cache: Persistent disk cache for HTTP errors with different expiration times
|
190
|
-
|
191
|
-
Also implements URL whitelist checking and browser-like behavior.
|
192
|
-
"""
|
193
|
-
# Check URL whitelist
|
194
|
-
from janito.tools.url_whitelist import get_url_whitelist_manager
|
195
|
-
|
196
|
-
whitelist_manager = get_url_whitelist_manager()
|
197
|
-
|
198
|
-
if not whitelist_manager.is_url_allowed(url):
|
199
|
-
error_message = tr("Blocked")
|
200
|
-
self.report_error(
|
201
|
-
tr("❗ Blocked"),
|
202
|
-
ReportAction.READ,
|
203
|
-
)
|
204
|
-
return error_message
|
205
|
-
|
206
|
-
# Check session cache first
|
207
|
-
if url in self.session_cache:
|
208
|
-
return self.session_cache[url]
|
209
|
-
|
210
|
-
# Check persistent cache for known errors
|
211
|
-
cached_error, is_cached = self._get_cached_error(url)
|
212
|
-
if cached_error:
|
213
|
-
self.report_warning(
|
214
|
-
tr(
|
215
|
-
"ℹ️ Using cached HTTP error for URL: {url}",
|
216
|
-
url=url,
|
217
|
-
),
|
218
|
-
ReportAction.READ,
|
219
|
-
)
|
220
|
-
return cached_error
|
221
|
-
|
222
|
-
try:
|
223
|
-
# Merge custom headers with default ones
|
224
|
-
request_headers = self.session.headers.copy()
|
225
|
-
if headers:
|
226
|
-
request_headers.update(headers)
|
227
|
-
|
228
|
-
# Merge custom cookies
|
229
|
-
if cookies:
|
230
|
-
self.session.cookies.update(cookies)
|
231
|
-
|
232
|
-
response = self.session.get(
|
233
|
-
url,
|
234
|
-
timeout=timeout,
|
235
|
-
headers=request_headers,
|
236
|
-
allow_redirects=follow_redirects,
|
237
|
-
)
|
238
|
-
response.raise_for_status()
|
239
|
-
content = response.text
|
240
|
-
|
241
|
-
# Save cookies after successful request
|
242
|
-
self._save_cookies()
|
243
|
-
|
244
|
-
# Cache successful responses in session cache
|
245
|
-
self.session_cache[url] = content
|
246
|
-
return content
|
247
|
-
except requests.exceptions.HTTPError as http_err:
|
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
|
-
|
272
|
-
if status_code and 400 <= status_code < 500:
|
273
|
-
description = status_descriptions.get(status_code, "Client Error")
|
274
|
-
error_message = f"HTTP {status_code} {description}"
|
275
|
-
# Cache 403 and 404 errors
|
276
|
-
if status_code in [403, 404]:
|
277
|
-
self._cache_error(url, status_code, error_message)
|
278
|
-
|
279
|
-
self.report_error(
|
280
|
-
f"❗ HTTP {status_code} {description}",
|
281
|
-
ReportAction.READ,
|
282
|
-
)
|
283
|
-
return error_message
|
284
|
-
else:
|
285
|
-
status_code_str = str(status_code) if status_code else "Error"
|
286
|
-
description = status_descriptions.get(
|
287
|
-
status_code,
|
288
|
-
(
|
289
|
-
"Server Error"
|
290
|
-
if status_code and status_code >= 500
|
291
|
-
else "Client Error"
|
292
|
-
),
|
293
|
-
)
|
294
|
-
self.report_error(
|
295
|
-
f"❗ HTTP {status_code_str} {description}",
|
296
|
-
ReportAction.READ,
|
297
|
-
)
|
298
|
-
return f"HTTP {status_code_str} {description}"
|
299
|
-
except requests.exceptions.ConnectionError as conn_err:
|
300
|
-
self.report_error(
|
301
|
-
"❗ Network Error",
|
302
|
-
ReportAction.READ,
|
303
|
-
)
|
304
|
-
return f"Network Error: Failed to connect to {url}"
|
305
|
-
except requests.exceptions.Timeout as timeout_err:
|
306
|
-
self.report_error(
|
307
|
-
"❗ Timeout Error",
|
308
|
-
ReportAction.READ,
|
309
|
-
)
|
310
|
-
return f"Timeout Error: Request timed out after {timeout} seconds"
|
311
|
-
except requests.exceptions.RequestException as req_err:
|
312
|
-
self.report_error(
|
313
|
-
"❗ Request Error",
|
314
|
-
ReportAction.READ,
|
315
|
-
)
|
316
|
-
return f"Request Error: {str(req_err)}"
|
317
|
-
except Exception as err:
|
318
|
-
self.report_error(
|
319
|
-
"❗ Error fetching URL",
|
320
|
-
ReportAction.READ,
|
321
|
-
)
|
322
|
-
return f"Error: {str(err)}"
|
323
|
-
|
324
|
-
def _extract_and_clean_text(self, html_content: str) -> str:
|
325
|
-
"""Extract and clean text from HTML content."""
|
326
|
-
soup = BeautifulSoup(html_content, "html.parser")
|
327
|
-
text = soup.get_text(separator="\n")
|
328
|
-
|
329
|
-
# Clean up excessive whitespace
|
330
|
-
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
331
|
-
return "\n".join(lines)
|
332
|
-
|
333
|
-
def _filter_by_search_strings(
|
334
|
-
self, text: str, search_strings: list[str], context_chars: int
|
335
|
-
) -> str:
|
336
|
-
"""Filter text by search strings with context."""
|
337
|
-
filtered = []
|
338
|
-
for s in search_strings:
|
339
|
-
idx = text.find(s)
|
340
|
-
if idx != -1:
|
341
|
-
start = max(0, idx - context_chars)
|
342
|
-
end = min(len(text), idx + len(s) + context_chars)
|
343
|
-
snippet = text[start:end]
|
344
|
-
filtered.append(snippet)
|
345
|
-
|
346
|
-
if filtered:
|
347
|
-
return "\n...\n".join(filtered)
|
348
|
-
else:
|
349
|
-
return tr("No lines found for the provided search strings.")
|
350
|
-
|
351
|
-
def _apply_limits(self, text: str, max_length: int, max_lines: int) -> str:
|
352
|
-
"""Apply length and line limits to text."""
|
353
|
-
# Apply length limit
|
354
|
-
if len(text) > max_length:
|
355
|
-
text = text[:max_length] + "\n... (content truncated due to length limit)"
|
356
|
-
|
357
|
-
# Apply line limit
|
358
|
-
lines = text.splitlines()
|
359
|
-
if len(lines) > max_lines:
|
360
|
-
text = (
|
361
|
-
"\n".join(lines[:max_lines])
|
362
|
-
+ "\n... (content truncated due to line limit)"
|
363
|
-
)
|
364
|
-
|
365
|
-
return text
|
366
|
-
|
367
|
-
@protect_against_loops(max_calls=5, time_window=10.0, key_field="url")
|
368
|
-
def run(
|
369
|
-
self,
|
370
|
-
url: str,
|
371
|
-
search_strings: list[str] = None,
|
372
|
-
max_length: int = 5000,
|
373
|
-
max_lines: int = 200,
|
374
|
-
context_chars: int = 400,
|
375
|
-
timeout: int = 10,
|
376
|
-
save_to_file: str = None,
|
377
|
-
headers: Dict[str, str] = None,
|
378
|
-
cookies: Dict[str, str] = None,
|
379
|
-
follow_redirects: bool = True,
|
380
|
-
) -> str:
|
381
|
-
if not url.strip():
|
382
|
-
self.report_warning(tr("ℹ️ Empty URL provided."), ReportAction.READ)
|
383
|
-
return tr("Warning: Empty URL provided. Operation skipped.")
|
384
|
-
|
385
|
-
self.report_action(tr("🌐 Fetch URL '{url}' ...", url=url), ReportAction.READ)
|
386
|
-
|
387
|
-
# Check if we should save to file
|
388
|
-
if save_to_file:
|
389
|
-
html_content = self._fetch_url_content(
|
390
|
-
url,
|
391
|
-
timeout=timeout,
|
392
|
-
headers=headers,
|
393
|
-
cookies=cookies,
|
394
|
-
follow_redirects=follow_redirects,
|
395
|
-
)
|
396
|
-
if (
|
397
|
-
html_content.startswith("HTTP Error ")
|
398
|
-
or html_content == "Error"
|
399
|
-
or html_content == "Blocked"
|
400
|
-
):
|
401
|
-
return html_content
|
402
|
-
|
403
|
-
try:
|
404
|
-
with open(save_to_file, "w", encoding="utf-8") as f:
|
405
|
-
f.write(html_content)
|
406
|
-
file_size = len(html_content)
|
407
|
-
self.report_success(
|
408
|
-
tr(
|
409
|
-
"✅ Saved {size} bytes to {file}",
|
410
|
-
size=file_size,
|
411
|
-
file=save_to_file,
|
412
|
-
),
|
413
|
-
ReportAction.READ,
|
414
|
-
)
|
415
|
-
return tr("Successfully saved content to: {file}", file=save_to_file)
|
416
|
-
except IOError as e:
|
417
|
-
error_msg = tr("Error saving to file: {error}", error=str(e))
|
418
|
-
self.report_error(error_msg, ReportAction.READ)
|
419
|
-
return error_msg
|
420
|
-
|
421
|
-
# Normal processing path
|
422
|
-
html_content = self._fetch_url_content(
|
423
|
-
url,
|
424
|
-
timeout=timeout,
|
425
|
-
headers=headers,
|
426
|
-
cookies=cookies,
|
427
|
-
follow_redirects=follow_redirects,
|
428
|
-
)
|
429
|
-
if (
|
430
|
-
html_content.startswith("HTTP Error ")
|
431
|
-
or html_content == "Error"
|
432
|
-
or html_content == "Blocked"
|
433
|
-
):
|
434
|
-
return html_content
|
435
|
-
|
436
|
-
# Extract and clean text
|
437
|
-
text = self._extract_and_clean_text(html_content)
|
438
|
-
|
439
|
-
# Filter by search strings if provided
|
440
|
-
if search_strings:
|
441
|
-
text = self._filter_by_search_strings(text, search_strings, context_chars)
|
442
|
-
|
443
|
-
# Apply limits
|
444
|
-
text = self._apply_limits(text, max_length, max_lines)
|
445
|
-
|
446
|
-
# Report success
|
447
|
-
num_lines = len(text.splitlines())
|
448
|
-
total_chars = len(text)
|
449
|
-
self.report_success(
|
450
|
-
tr(
|
451
|
-
"✅ {num_lines} {line_word}, {chars} chars",
|
452
|
-
num_lines=num_lines,
|
453
|
-
line_word=pluralize("line", num_lines),
|
454
|
-
chars=total_chars,
|
455
|
-
),
|
456
|
-
ReportAction.READ,
|
457
|
-
)
|
458
|
-
return text
|
@@ -1,51 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import webbrowser
|
3
|
-
from janito.tools.adapters.local.adapter import register_local_tool
|
4
|
-
from janito.tools.tool_base import ToolBase, ToolPermissions
|
5
|
-
from janito.report_events import ReportAction
|
6
|
-
from janito.i18n import tr
|
7
|
-
from janito.tools.loop_protection_decorator import protect_against_loops
|
8
|
-
|
9
|
-
|
10
|
-
@register_local_tool
|
11
|
-
class OpenHtmlInBrowserTool(ToolBase):
|
12
|
-
"""
|
13
|
-
Open the supplied HTML file in the default web browser.
|
14
|
-
|
15
|
-
Args:
|
16
|
-
path (str): Path to the HTML file to open.
|
17
|
-
Returns:
|
18
|
-
str: Status message indicating the result.
|
19
|
-
"""
|
20
|
-
|
21
|
-
permissions = ToolPermissions(read=True)
|
22
|
-
tool_name = "open_html_in_browser"
|
23
|
-
|
24
|
-
@protect_against_loops(max_calls=5, time_window=10.0, key_field="path")
|
25
|
-
def run(self, path: str) -> str:
|
26
|
-
if not path.strip():
|
27
|
-
self.report_warning(tr("ℹ️ Empty file path provided."))
|
28
|
-
return tr("Warning: Empty file path provided. Operation skipped.")
|
29
|
-
if not os.path.isfile(path):
|
30
|
-
self.report_error(tr("❗ File does not exist: {path}", path=path))
|
31
|
-
return tr("Warning: File does not exist: {path}", path=path)
|
32
|
-
if not path.lower().endswith((".html", ".htm")):
|
33
|
-
self.report_warning(tr("⚠️ Not an HTML file: {path}", path=path))
|
34
|
-
return tr("Warning: Not an HTML file: {path}", path=path)
|
35
|
-
url = "file://" + os.path.abspath(path)
|
36
|
-
self.report_action(
|
37
|
-
tr("📖 Opening HTML file in browser: {path}", path=path), ReportAction.READ
|
38
|
-
)
|
39
|
-
try:
|
40
|
-
webbrowser.open(url)
|
41
|
-
except Exception as err:
|
42
|
-
self.report_error(
|
43
|
-
tr("❗ Error opening HTML file: {path}: {err}", path=path, err=str(err))
|
44
|
-
)
|
45
|
-
return tr(
|
46
|
-
"Warning: Error opening HTML file: {path}: {err}",
|
47
|
-
path=path,
|
48
|
-
err=str(err),
|
49
|
-
)
|
50
|
-
self.report_success(tr("✅ HTML file opened in browser: {path}", path=path))
|
51
|
-
return tr("HTML file opened in browser: {path}", path=path)
|