janito 1.2.1__py3-none-any.whl → 1.3.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/__init__.py +1 -1
- janito/agent/agent.py +23 -10
- janito/agent/config.py +37 -9
- janito/agent/conversation.py +8 -0
- janito/agent/runtime_config.py +1 -0
- janito/agent/tool_handler.py +154 -52
- janito/agent/tools/__init__.py +8 -5
- janito/agent/tools/ask_user.py +2 -1
- janito/agent/tools/fetch_url.py +27 -35
- janito/agent/tools/file_ops.py +72 -67
- janito/agent/tools/find_files.py +47 -26
- janito/agent/tools/get_lines.py +58 -0
- janito/agent/tools/py_compile.py +26 -0
- janito/agent/tools/python_exec.py +47 -0
- janito/agent/tools/remove_directory.py +38 -0
- janito/agent/tools/replace_text_in_file.py +67 -0
- janito/agent/tools/run_bash_command.py +134 -0
- janito/agent/tools/search_files.py +52 -0
- janito/cli/_print_config.py +1 -1
- janito/cli/arg_parser.py +6 -1
- janito/cli/config_commands.py +56 -8
- janito/cli/runner.py +21 -9
- janito/cli_chat_shell/chat_loop.py +5 -3
- janito/cli_chat_shell/commands.py +34 -37
- janito/cli_chat_shell/config_shell.py +1 -1
- janito/cli_chat_shell/load_prompt.py +1 -1
- janito/cli_chat_shell/session_manager.py +11 -15
- janito/cli_chat_shell/ui.py +17 -8
- janito/render_prompt.py +3 -1
- janito/web/app.py +1 -1
- janito-1.3.0.dist-info/METADATA +142 -0
- janito-1.3.0.dist-info/RECORD +51 -0
- janito/agent/tools/bash_exec.py +0 -58
- janito/agent/tools/file_str_replace.py +0 -48
- janito/agent/tools/search_text.py +0 -41
- janito/agent/tools/view_file.py +0 -34
- janito/templates/system_instructions.j2 +0 -38
- janito-1.2.1.dist-info/METADATA +0 -85
- janito-1.2.1.dist-info/RECORD +0 -49
- {janito-1.2.1.dist-info → janito-1.3.0.dist-info}/WHEEL +0 -0
- {janito-1.2.1.dist-info → janito-1.3.0.dist-info}/entry_points.txt +0 -0
- {janito-1.2.1.dist-info → janito-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.2.1.dist-info → janito-1.3.0.dist-info}/top_level.txt +0 -0
janito/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "1.
|
1
|
+
__version__ = "1.3.0"
|
janito/agent/agent.py
CHANGED
@@ -7,7 +7,7 @@ from janito.agent.conversation import ConversationHandler
|
|
7
7
|
from janito.agent.tool_handler import ToolHandler
|
8
8
|
|
9
9
|
class Agent:
|
10
|
-
"""
|
10
|
+
"""Agent capable of handling conversations and tool calls."""
|
11
11
|
|
12
12
|
REFERER = "www.janito.dev"
|
13
13
|
TITLE = "Janito"
|
@@ -19,10 +19,22 @@ class Agent:
|
|
19
19
|
system_prompt: str | None = None,
|
20
20
|
verbose_tools: bool = False,
|
21
21
|
tool_handler = None,
|
22
|
-
base_url: str = "https://openrouter.ai/api/v1"
|
22
|
+
base_url: str = "https://openrouter.ai/api/v1",
|
23
|
+
azure_openai_api_version: str = "2023-05-15",
|
24
|
+
use_azure_openai: bool = False
|
23
25
|
):
|
24
26
|
"""
|
25
|
-
Initialize
|
27
|
+
Initialize Agent,
|
28
|
+
|
29
|
+
Args:
|
30
|
+
api_key: API key for OpenAI-compatible service.
|
31
|
+
model: Model name to use.
|
32
|
+
system_prompt: Optional system prompt override.
|
33
|
+
verbose_tools: Enable verbose tool call logging.
|
34
|
+
tool_handler: Optional custom ToolHandler instance.
|
35
|
+
base_url: API base URL.
|
36
|
+
azure_openai_api_version: Azure OpenAI API version (default: "2023-05-15").
|
37
|
+
use_azure_openai: Whether to use Azure OpenAI client (default: False).
|
26
38
|
|
27
39
|
Args:
|
28
40
|
api_key: API key for OpenAI-compatible service.
|
@@ -35,12 +47,12 @@ class Agent:
|
|
35
47
|
self.api_key = api_key
|
36
48
|
self.model = model
|
37
49
|
self.system_prompt = system_prompt
|
38
|
-
if
|
50
|
+
if use_azure_openai:
|
39
51
|
from openai import AzureOpenAI
|
40
52
|
self.client = AzureOpenAI(
|
41
53
|
api_key=api_key,
|
42
|
-
azure_endpoint=
|
43
|
-
api_version=
|
54
|
+
azure_endpoint=base_url,
|
55
|
+
api_version=azure_openai_api_version,
|
44
56
|
)
|
45
57
|
else:
|
46
58
|
self.client = OpenAI(
|
@@ -64,7 +76,7 @@ class Agent:
|
|
64
76
|
def usage_history(self):
|
65
77
|
return self.conversation_handler.usage_history
|
66
78
|
|
67
|
-
def chat(self, messages, on_content=None, on_tool_progress=None, verbose_response=False, spinner=False, max_tokens=None):
|
79
|
+
def chat(self, messages, on_content=None, on_tool_progress=None, verbose_response=False, spinner=False, max_tokens=None, max_rounds=50):
|
68
80
|
import time
|
69
81
|
from janito.agent.conversation import ProviderError
|
70
82
|
|
@@ -73,11 +85,12 @@ class Agent:
|
|
73
85
|
try:
|
74
86
|
return self.conversation_handler.handle_conversation(
|
75
87
|
messages,
|
76
|
-
|
88
|
+
max_rounds=max_rounds,
|
77
89
|
on_content=on_content,
|
78
90
|
on_tool_progress=on_tool_progress,
|
79
91
|
verbose_response=verbose_response,
|
80
|
-
spinner=spinner
|
92
|
+
spinner=spinner,
|
93
|
+
max_tokens=max_tokens
|
81
94
|
)
|
82
95
|
except ProviderError as e:
|
83
96
|
error_data = getattr(e, 'error_data', {}) or {}
|
@@ -96,5 +109,5 @@ class Agent:
|
|
96
109
|
else:
|
97
110
|
print("Max retries reached. Raising error.")
|
98
111
|
raise
|
99
|
-
|
112
|
+
|
100
113
|
raise
|
janito/agent/config.py
CHANGED
@@ -40,14 +40,12 @@ class FileConfig(BaseConfig):
|
|
40
40
|
|
41
41
|
def load(self):
|
42
42
|
if self.path.exists():
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
print(f"Warning: Failed to load config file {self.path}: {e}")
|
50
|
-
self._data = {}
|
43
|
+
with open(self.path, 'r') as f:
|
44
|
+
self._data = json.load(f)
|
45
|
+
# Remove keys with value None (null in JSON)
|
46
|
+
self._data = {k: v for k, v in self._data.items() if v is not None}
|
47
|
+
|
48
|
+
|
51
49
|
else:
|
52
50
|
self._data = {}
|
53
51
|
|
@@ -57,6 +55,7 @@ class FileConfig(BaseConfig):
|
|
57
55
|
json.dump(self._data, f, indent=2)
|
58
56
|
|
59
57
|
|
58
|
+
|
60
59
|
CONFIG_OPTIONS = {
|
61
60
|
"api_key": "API key for OpenAI-compatible service (required)",
|
62
61
|
"model": "Model name to use (e.g., 'openai/gpt-4.1')",
|
@@ -64,9 +63,38 @@ CONFIG_OPTIONS = {
|
|
64
63
|
"role": "Role description for the system prompt (e.g., 'software engineer')",
|
65
64
|
"system_prompt": "Override the entire system prompt text",
|
66
65
|
"temperature": "Sampling temperature (float, e.g., 0.0 - 2.0)",
|
67
|
-
"max_tokens": "Maximum tokens for model response (int)"
|
66
|
+
"max_tokens": "Maximum tokens for model response (int)",
|
67
|
+
# Accept template.* keys as valid config keys (for CLI validation, etc.)
|
68
|
+
"template": "Template context dictionary for prompt rendering (nested)",
|
69
|
+
# Note: template.* keys are validated dynamically, not statically here
|
68
70
|
}
|
69
71
|
|
72
|
+
class BaseConfig:
|
73
|
+
def __init__(self):
|
74
|
+
self._data = {}
|
75
|
+
|
76
|
+
def get(self, key, default=None):
|
77
|
+
return self._data.get(key, default)
|
78
|
+
|
79
|
+
def set(self, key, value):
|
80
|
+
self._data[key] = value
|
81
|
+
|
82
|
+
def all(self):
|
83
|
+
return self._data
|
84
|
+
|
85
|
+
|
86
|
+
"""
|
87
|
+
Returns a dictionary suitable for passing as Jinja2 template variables.
|
88
|
+
Merges the nested 'template' dict (if present) and all flat 'template.*' keys.
|
89
|
+
Flat keys override nested dict keys if there is a conflict.
|
90
|
+
"""
|
91
|
+
template_vars = dict(self._data.get("template", {}))
|
92
|
+
for k, v in self._data.items():
|
93
|
+
if k.startswith("template.") and k != "template":
|
94
|
+
template_vars[k[9:]] = v
|
95
|
+
return template_vars
|
96
|
+
|
97
|
+
|
70
98
|
# Import defaults for reference
|
71
99
|
from .config_defaults import CONFIG_DEFAULTS
|
72
100
|
|
janito/agent/conversation.py
CHANGED
@@ -19,6 +19,9 @@ class ConversationHandler:
|
|
19
19
|
self.usage_history = []
|
20
20
|
|
21
21
|
def handle_conversation(self, messages, max_rounds=50, on_content=None, on_tool_progress=None, verbose_response=False, spinner=False, max_tokens=None):
|
22
|
+
from janito.agent.runtime_config import runtime_config
|
23
|
+
max_tools = runtime_config.get('max_tools', None)
|
24
|
+
tool_calls_made = 0
|
22
25
|
if not messages:
|
23
26
|
raise ValueError("No prompt provided in messages")
|
24
27
|
|
@@ -105,10 +108,15 @@ class ConversationHandler:
|
|
105
108
|
"usage_history": self.usage_history
|
106
109
|
}
|
107
110
|
|
111
|
+
from janito.agent.runtime_config import runtime_config
|
108
112
|
tool_responses = []
|
113
|
+
# Sequential tool execution (default, only mode)
|
109
114
|
for tool_call in choice.message.tool_calls:
|
115
|
+
if max_tools is not None and tool_calls_made >= max_tools:
|
116
|
+
raise MaxRoundsExceededError(f"Maximum number of tool calls ({max_tools}) reached in this chat session.")
|
110
117
|
result = self.tool_handler.handle_tool_call(tool_call, on_progress=on_tool_progress)
|
111
118
|
tool_responses.append({"tool_call_id": tool_call.id, "content": result})
|
119
|
+
tool_calls_made += 1
|
112
120
|
|
113
121
|
# Store usage info in usage_history, linked to the next assistant message index
|
114
122
|
assistant_idx = len([m for m in messages if m.get('role') == 'assistant'])
|
janito/agent/runtime_config.py
CHANGED
janito/agent/tool_handler.py
CHANGED
@@ -8,6 +8,8 @@ class ToolHandler:
|
|
8
8
|
@classmethod
|
9
9
|
def register_tool(cls, func):
|
10
10
|
import inspect
|
11
|
+
import typing
|
12
|
+
from typing import get_origin, get_args
|
11
13
|
|
12
14
|
name = func.__name__
|
13
15
|
description = func.__doc__ or ""
|
@@ -23,18 +25,126 @@ class ToolHandler:
|
|
23
25
|
if param.annotation is param.empty:
|
24
26
|
raise TypeError(f"Parameter '{param_name}' in tool '{name}' is missing a type hint.")
|
25
27
|
param_type = param.annotation
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
28
|
+
schema = {}
|
29
|
+
|
30
|
+
# Handle typing.Optional, typing.List, typing.Literal, etc.
|
31
|
+
origin = get_origin(param_type)
|
32
|
+
args = get_args(param_type)
|
33
|
+
|
34
|
+
if origin is typing.Union and type(None) in args:
|
35
|
+
# Optional[...] type
|
36
|
+
main_type = args[0] if args[1] is type(None) else args[1]
|
37
|
+
origin = get_origin(main_type)
|
38
|
+
args = get_args(main_type)
|
39
|
+
param_type = main_type
|
40
|
+
else:
|
41
|
+
main_type = param_type
|
42
|
+
|
43
|
+
if origin is list or origin is typing.List:
|
44
|
+
item_type = args[0] if args else str
|
45
|
+
item_schema = {"type": _pytype_to_json_type(item_type)}
|
46
|
+
schema = {"type": "array", "items": item_schema}
|
47
|
+
elif origin is typing.Literal:
|
48
|
+
schema = {"type": _pytype_to_json_type(type(args[0])), "enum": list(args)}
|
49
|
+
elif main_type == int:
|
50
|
+
schema = {"type": "integer"}
|
51
|
+
elif main_type == float:
|
52
|
+
schema = {"type": "number"}
|
53
|
+
elif main_type == bool:
|
54
|
+
schema = {"type": "boolean"}
|
55
|
+
elif main_type == dict:
|
56
|
+
schema = {"type": "object"}
|
57
|
+
elif main_type == list:
|
58
|
+
schema = {"type": "array", "items": {"type": "string"}}
|
59
|
+
else:
|
60
|
+
schema = {"type": "string"}
|
61
|
+
|
62
|
+
# Optionally add description if available in docstring (not implemented here)
|
63
|
+
params_schema["properties"][param_name] = schema
|
64
|
+
if param.default is param.empty:
|
65
|
+
params_schema["required"].append(param_name)
|
66
|
+
|
67
|
+
cls._tool_registry[name] = {
|
68
|
+
"function": func,
|
69
|
+
"description": description,
|
70
|
+
"parameters": params_schema
|
71
|
+
}
|
72
|
+
return func
|
73
|
+
|
74
|
+
def _pytype_to_json_type(pytype):
|
75
|
+
import typing
|
76
|
+
if pytype == int:
|
77
|
+
return "integer"
|
78
|
+
elif pytype == float:
|
79
|
+
return "number"
|
80
|
+
elif pytype == bool:
|
81
|
+
return "boolean"
|
82
|
+
elif pytype == dict:
|
83
|
+
return "object"
|
84
|
+
elif pytype == list or pytype == typing.List:
|
85
|
+
return "array"
|
86
|
+
else:
|
87
|
+
return "string"
|
88
|
+
|
89
|
+
class ToolHandler:
|
90
|
+
_tool_registry = {}
|
91
|
+
|
92
|
+
@classmethod
|
93
|
+
def register_tool(cls, func):
|
94
|
+
import inspect
|
95
|
+
import typing
|
96
|
+
from typing import get_origin, get_args
|
97
|
+
|
98
|
+
name = func.__name__
|
99
|
+
description = func.__doc__ or ""
|
100
|
+
|
101
|
+
sig = inspect.signature(func)
|
102
|
+
params_schema = {
|
103
|
+
"type": "object",
|
104
|
+
"properties": {},
|
105
|
+
"required": []
|
106
|
+
}
|
107
|
+
|
108
|
+
for param_name, param in sig.parameters.items():
|
109
|
+
if param.annotation is param.empty:
|
110
|
+
raise TypeError(f"Parameter '{param_name}' in tool '{name}' is missing a type hint.")
|
111
|
+
param_type = param.annotation
|
112
|
+
schema = {}
|
113
|
+
|
114
|
+
# Handle typing.Optional, typing.List, typing.Literal, etc.
|
115
|
+
origin = get_origin(param_type)
|
116
|
+
args = get_args(param_type)
|
117
|
+
|
118
|
+
if origin is typing.Union and type(None) in args:
|
119
|
+
# Optional[...] type
|
120
|
+
main_type = args[0] if args[1] is type(None) else args[1]
|
121
|
+
origin = get_origin(main_type)
|
122
|
+
args = get_args(main_type)
|
123
|
+
param_type = main_type
|
124
|
+
else:
|
125
|
+
main_type = param_type
|
126
|
+
|
127
|
+
if origin is list or origin is typing.List:
|
128
|
+
item_type = args[0] if args else str
|
129
|
+
item_schema = {"type": _pytype_to_json_type(item_type)}
|
130
|
+
schema = {"type": "array", "items": item_schema}
|
131
|
+
elif origin is typing.Literal:
|
132
|
+
schema = {"type": _pytype_to_json_type(type(args[0])), "enum": list(args)}
|
133
|
+
elif main_type == int:
|
134
|
+
schema = {"type": "integer"}
|
135
|
+
elif main_type == float:
|
136
|
+
schema = {"type": "number"}
|
137
|
+
elif main_type == bool:
|
138
|
+
schema = {"type": "boolean"}
|
139
|
+
elif main_type == dict:
|
140
|
+
schema = {"type": "object"}
|
141
|
+
elif main_type == list:
|
142
|
+
schema = {"type": "array", "items": {"type": "string"}}
|
143
|
+
else:
|
144
|
+
schema = {"type": "string"}
|
145
|
+
|
146
|
+
# Optionally add description if available in docstring (not implemented here)
|
147
|
+
params_schema["properties"][param_name] = schema
|
38
148
|
if param.default is param.empty:
|
39
149
|
params_schema["required"].append(param_name)
|
40
150
|
|
@@ -82,46 +192,38 @@ class ToolHandler:
|
|
82
192
|
args = json.loads(tool_call.function.arguments)
|
83
193
|
if self.verbose:
|
84
194
|
print(f"[Tool Call] {tool_call.function.name} called with arguments: {args}")
|
195
|
+
import inspect
|
196
|
+
sig = inspect.signature(func)
|
197
|
+
if on_progress:
|
198
|
+
on_progress({
|
199
|
+
'event': 'start',
|
200
|
+
'call_id': call_id,
|
201
|
+
'tool': tool_call.function.name,
|
202
|
+
'args': args
|
203
|
+
})
|
204
|
+
if 'on_progress' in sig.parameters and on_progress is not None:
|
205
|
+
args['on_progress'] = on_progress
|
85
206
|
try:
|
86
|
-
import inspect
|
87
|
-
sig = inspect.signature(func)
|
88
|
-
if on_progress:
|
89
|
-
on_progress({
|
90
|
-
'event': 'start',
|
91
|
-
'call_id': call_id,
|
92
|
-
'tool': tool_call.function.name,
|
93
|
-
'args': args
|
94
|
-
})
|
95
|
-
if 'on_progress' in sig.parameters and on_progress is not None:
|
96
|
-
args['on_progress'] = on_progress
|
97
207
|
result = func(**args)
|
98
|
-
if self.verbose:
|
99
|
-
preview = result
|
100
|
-
if isinstance(result, str):
|
101
|
-
lines = result.splitlines()
|
102
|
-
if len(lines) > 10:
|
103
|
-
preview = "\n".join(lines[:10]) + "\n... (truncated)"
|
104
|
-
elif len(result) > 500:
|
105
|
-
preview = result[:500] + "... (truncated)"
|
106
|
-
print(f"[Tool Result] {tool_call.function.name} returned:\n{preview}")
|
107
|
-
if on_progress:
|
108
|
-
on_progress({
|
109
|
-
'event': 'finish',
|
110
|
-
'call_id': call_id,
|
111
|
-
'tool': tool_call.function.name,
|
112
|
-
'args': args,
|
113
|
-
'result': result
|
114
|
-
})
|
115
|
-
return result
|
116
208
|
except Exception as e:
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
209
|
+
import traceback
|
210
|
+
error_message = f"[Tool Error] {type(e).__name__}: {e}\n" + traceback.format_exc()
|
211
|
+
result = error_message
|
212
|
+
if self.verbose:
|
213
|
+
preview = result
|
214
|
+
if isinstance(result, str):
|
215
|
+
lines = result.splitlines()
|
216
|
+
if len(lines) > 10:
|
217
|
+
preview = "\n".join(lines[:10]) + "\n... (truncated)"
|
218
|
+
elif len(result) > 500:
|
219
|
+
preview = result[:500] + "... (truncated)"
|
220
|
+
print(f"[Tool Result] {tool_call.function.name} returned:\n{preview}")
|
221
|
+
if on_progress:
|
222
|
+
on_progress({
|
223
|
+
'event': 'finish',
|
224
|
+
'call_id': call_id,
|
225
|
+
'tool': tool_call.function.name,
|
226
|
+
'args': args,
|
227
|
+
'result': result
|
228
|
+
})
|
229
|
+
return result
|
janito/agent/tools/__init__.py
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
from .ask_user import ask_user
|
2
|
-
from .file_ops import
|
3
|
-
from .
|
2
|
+
from .file_ops import create_file, create_directory, remove_file, move_file
|
3
|
+
from .get_lines import get_lines
|
4
|
+
from .replace_text_in_file import replace_text_in_file
|
4
5
|
from .find_files import find_files
|
5
|
-
from .
|
6
|
-
from .bash_exec import bash_exec
|
6
|
+
from .run_bash_command import run_bash_command
|
7
7
|
from .fetch_url import fetch_url
|
8
|
+
from .python_exec import python_exec
|
9
|
+
from .py_compile import py_compile_file
|
10
|
+
from .search_files import search_files
|
11
|
+
from .remove_directory import remove_directory
|
8
12
|
|
9
|
-
from .file_str_replace import file_str_replace
|
janito/agent/tools/ask_user.py
CHANGED
@@ -11,7 +11,8 @@ def ask_user(question: str) -> str:
|
|
11
11
|
"""
|
12
12
|
Ask the user a question and return their response.
|
13
13
|
|
14
|
-
|
14
|
+
Args:
|
15
|
+
question (str): The question to ask the user.
|
15
16
|
"""
|
16
17
|
from rich import print as rich_print
|
17
18
|
from rich.panel import Panel
|
janito/agent/tools/fetch_url.py
CHANGED
@@ -1,48 +1,40 @@
|
|
1
1
|
import requests
|
2
|
+
from typing import Optional, Callable
|
2
3
|
from bs4 import BeautifulSoup
|
3
4
|
from janito.agent.tool_handler import ToolHandler
|
4
5
|
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
5
6
|
|
6
7
|
@ToolHandler.register_tool
|
7
|
-
def fetch_url(url: str, search_strings: list[str] = None, on_progress:
|
8
|
+
def fetch_url(url: str, search_strings: list[str] = None, on_progress: Optional[Callable[[dict], None]] = None) -> str:
|
8
9
|
"""
|
9
10
|
Fetch the content of a web page and extract its text.
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
Args:
|
13
|
+
url (str): The URL to fetch.
|
14
|
+
search_strings (list[str], optional): List of strings to filter the extracted text around those strings.
|
15
|
+
on_progress (callable, optional): Callback function for streaming progress updates.
|
14
16
|
"""
|
15
|
-
if on_progress:
|
16
|
-
on_progress({'event': 'start', 'url': url})
|
17
17
|
print_info(f"\U0001F310 Fetching URL: {url} ... ")
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
text = soup.get_text(separator=' ', strip=True)
|
18
|
+
response = requests.get(url, timeout=10)
|
19
|
+
response.raise_for_status()
|
20
|
+
if on_progress:
|
21
|
+
on_progress({'event': 'fetched', 'status_code': response.status_code})
|
22
|
+
soup = BeautifulSoup(response.text, 'html.parser')
|
23
|
+
text = soup.get_text(separator='\n')
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
25
|
+
if search_strings:
|
26
|
+
filtered = []
|
27
|
+
for s in search_strings:
|
28
|
+
idx = text.find(s)
|
29
|
+
if idx != -1:
|
30
|
+
start = max(0, idx - 200)
|
31
|
+
end = min(len(text), idx + len(s) + 200)
|
32
|
+
snippet = text[start:end]
|
33
|
+
filtered.append(snippet)
|
34
|
+
if filtered:
|
35
|
+
text = '\n...\n'.join(filtered)
|
36
|
+
else:
|
37
|
+
text = "No matches found for the provided search strings."
|
39
38
|
|
40
|
-
|
41
|
-
|
42
|
-
on_progress({'event': 'done'})
|
43
|
-
return text
|
44
|
-
except Exception as e:
|
45
|
-
print_error(f"\u274c Error: {e}")
|
46
|
-
if on_progress:
|
47
|
-
on_progress({'event': 'error', 'error': str(e)})
|
48
|
-
return f"\u274c Failed to fetch URL '{url}': {e}"
|
39
|
+
print_success("\u2705 Success")
|
40
|
+
return text
|