janito 2.20.1__py3-none-any.whl → 2.22.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 +47 -4
- janito/agent/setup_agent.py +34 -4
- janito/agent/templates/profiles/system_prompt_template_Developer_with_Python_Tools.txt.j2 +0 -0
- janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +0 -0
- janito/agent/templates/profiles/system_prompt_template_market_analyst.txt.j2 +10 -0
- janito/agent/templates/profiles/system_prompt_template_model_conversation_without_tools_or_context.txt.j2 +0 -0
- janito/cli/chat_mode/session_profile_select.py +20 -3
- 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_profiles.py +29 -1
- janito/cli/cli_commands/show_system_prompt.py +45 -4
- janito/docs/GETTING_STARTED.md +85 -12
- 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/providers/__init__.py +1 -0
- janito/providers/azure_openai/provider.py +1 -1
- janito/providers/dashscope.bak.zip +0 -0
- janito/providers/ibm/README.md +99 -0
- janito/providers/ibm/__init__.py +1 -0
- janito/providers/ibm/model_info.py +87 -0
- janito/providers/ibm/provider.py +149 -0
- janito/shell.bak.zip +0 -0
- janito/tools/DOCSTRING_STANDARD.txt +0 -0
- janito/tools/README.md +0 -0
- janito/tools/adapters/local/fetch_url.py +175 -25
- janito/tools/outline_file.bak.zip +0 -0
- {janito-2.20.1.dist-info → janito-2.22.0.dist-info}/METADATA +411 -411
- {janito-2.20.1.dist-info → janito-2.22.0.dist-info}/RECORD +20 -15
- {janito-2.20.1.dist-info → janito-2.22.0.dist-info}/entry_points.txt +0 -0
- {janito-2.20.1.dist-info → janito-2.22.0.dist-info}/licenses/LICENSE +0 -0
- {janito-2.20.1.dist-info → janito-2.22.0.dist-info}/top_level.txt +0 -0
- {janito-2.20.1.dist-info → janito-2.22.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
"""IBM WatsonX AI Provider implementation."""
|
2
|
+
|
3
|
+
from janito.llm.provider import LLMProvider
|
4
|
+
from janito.llm.auth import LLMAuthManager
|
5
|
+
from janito.llm.driver_config import LLMDriverConfig
|
6
|
+
from janito.tools import get_local_tools_adapter
|
7
|
+
from janito.providers.registry import LLMProviderRegistry
|
8
|
+
from .model_info import MODEL_SPECS
|
9
|
+
|
10
|
+
try:
|
11
|
+
from janito.drivers.openai.driver import OpenAIModelDriver
|
12
|
+
|
13
|
+
available = True
|
14
|
+
unavailable_reason = None
|
15
|
+
except ImportError as e:
|
16
|
+
available = False
|
17
|
+
unavailable_reason = str(e)
|
18
|
+
|
19
|
+
|
20
|
+
class IBMProvider(LLMProvider):
|
21
|
+
"""IBM WatsonX AI Provider for accessing IBM's AI services."""
|
22
|
+
|
23
|
+
name = "ibm"
|
24
|
+
NAME = "ibm"
|
25
|
+
MAINTAINER = "João Pinto <janito@ikignosis.org>"
|
26
|
+
MODEL_SPECS = MODEL_SPECS
|
27
|
+
DEFAULT_MODEL = "ibm/granite-3-3-8b-instruct"
|
28
|
+
|
29
|
+
def __init__(
|
30
|
+
self, auth_manager: LLMAuthManager = None, config: LLMDriverConfig = None
|
31
|
+
):
|
32
|
+
self._tools_adapter = get_local_tools_adapter()
|
33
|
+
self._driver = None
|
34
|
+
|
35
|
+
if not self.available:
|
36
|
+
return
|
37
|
+
|
38
|
+
self._initialize_config(auth_manager, config)
|
39
|
+
self._setup_model_config()
|
40
|
+
|
41
|
+
def _initialize_config(self, auth_manager, config):
|
42
|
+
"""Initialize configuration and API credentials."""
|
43
|
+
self.auth_manager = auth_manager or LLMAuthManager()
|
44
|
+
|
45
|
+
# IBM WatsonX uses multiple credentials
|
46
|
+
self._api_key = self.auth_manager.get_credentials(type(self).NAME)
|
47
|
+
if not self._api_key:
|
48
|
+
from janito.llm.auth_utils import handle_missing_api_key
|
49
|
+
|
50
|
+
handle_missing_api_key(self.name, "WATSONX_API_KEY")
|
51
|
+
|
52
|
+
# Get project ID for WatsonX
|
53
|
+
self._project_id = self.auth_manager.get_credentials(
|
54
|
+
f"{type(self).NAME}_project_id"
|
55
|
+
)
|
56
|
+
if not self._project_id:
|
57
|
+
from janito.llm.auth_utils import handle_missing_api_key
|
58
|
+
|
59
|
+
handle_missing_api_key(self.name, "WATSONX_PROJECT_ID")
|
60
|
+
|
61
|
+
# Get region/space ID
|
62
|
+
self._space_id = self.auth_manager.get_credentials(
|
63
|
+
f"{type(self).NAME}_space_id"
|
64
|
+
)
|
65
|
+
|
66
|
+
self._driver_config = config or LLMDriverConfig(model=None)
|
67
|
+
if not self._driver_config.model:
|
68
|
+
self._driver_config.model = self.DEFAULT_MODEL
|
69
|
+
if not self._driver_config.api_key:
|
70
|
+
self._driver_config.api_key = self._api_key
|
71
|
+
|
72
|
+
def _setup_model_config(self):
|
73
|
+
"""Configure token limits based on model specifications."""
|
74
|
+
model_name = self._driver_config.model
|
75
|
+
model_spec = self.MODEL_SPECS.get(model_name)
|
76
|
+
|
77
|
+
# Reset token parameters
|
78
|
+
if hasattr(self._driver_config, "max_tokens"):
|
79
|
+
self._driver_config.max_tokens = None
|
80
|
+
if hasattr(self._driver_config, "max_completion_tokens"):
|
81
|
+
self._driver_config.max_completion_tokens = None
|
82
|
+
|
83
|
+
if model_spec:
|
84
|
+
if getattr(model_spec, "thinking_supported", False):
|
85
|
+
max_cot = getattr(model_spec, "max_cot", None)
|
86
|
+
if max_cot and max_cot != "N/A":
|
87
|
+
self._driver_config.max_completion_tokens = int(max_cot)
|
88
|
+
else:
|
89
|
+
max_response = getattr(model_spec, "max_response", None)
|
90
|
+
if max_response and max_response != "N/A":
|
91
|
+
self._driver_config.max_tokens = int(max_response)
|
92
|
+
|
93
|
+
# Set IBM WatsonX specific parameters
|
94
|
+
self._driver_config.base_url = "https://us-south.ml.cloud.ibm.com"
|
95
|
+
self._driver_config.project_id = self._project_id
|
96
|
+
if self._space_id:
|
97
|
+
self._driver_config.space_id = self._space_id
|
98
|
+
|
99
|
+
self.fill_missing_device_info(self._driver_config)
|
100
|
+
|
101
|
+
@property
|
102
|
+
def driver(self):
|
103
|
+
if not self.available:
|
104
|
+
raise ImportError(f"IBMProvider unavailable: {self.unavailable_reason}")
|
105
|
+
return self._driver
|
106
|
+
|
107
|
+
@property
|
108
|
+
def available(self):
|
109
|
+
return available
|
110
|
+
|
111
|
+
@property
|
112
|
+
def unavailable_reason(self):
|
113
|
+
return unavailable_reason
|
114
|
+
|
115
|
+
def create_driver(self):
|
116
|
+
"""
|
117
|
+
Creates and returns a new OpenAIModelDriver instance configured for IBM WatsonX.
|
118
|
+
IBM WatsonX uses OpenAI-compatible API format.
|
119
|
+
"""
|
120
|
+
driver = OpenAIModelDriver(
|
121
|
+
tools_adapter=self._tools_adapter, provider_name=self.NAME
|
122
|
+
)
|
123
|
+
driver.config = self._driver_config
|
124
|
+
return driver
|
125
|
+
|
126
|
+
def create_agent(self, tools_adapter=None, agent_name: str = None, **kwargs):
|
127
|
+
from janito.llm.agent import LLMAgent
|
128
|
+
|
129
|
+
if tools_adapter is None:
|
130
|
+
tools_adapter = get_local_tools_adapter()
|
131
|
+
raise NotImplementedError(
|
132
|
+
"create_agent must be constructed via new factory using input/output queues and config."
|
133
|
+
)
|
134
|
+
|
135
|
+
@property
|
136
|
+
def model_name(self):
|
137
|
+
return self._driver_config.model
|
138
|
+
|
139
|
+
@property
|
140
|
+
def driver_config(self):
|
141
|
+
"""Public, read-only access to the provider's LLMDriverConfig object."""
|
142
|
+
return self._driver_config
|
143
|
+
|
144
|
+
def execute_tool(self, tool_name: str, event_bus, *args, **kwargs):
|
145
|
+
self._tools_adapter.event_bus = event_bus
|
146
|
+
return self._tools_adapter.execute_by_name(tool_name, *args, **kwargs)
|
147
|
+
|
148
|
+
|
149
|
+
LLMProviderRegistry.register(IBMProvider.NAME, IBMProvider)
|
janito/shell.bak.zip
CHANGED
File without changes
|
File without changes
|
janito/tools/README.md
CHANGED
File without changes
|
@@ -1,4 +1,8 @@
|
|
1
1
|
import requests
|
2
|
+
import time
|
3
|
+
import os
|
4
|
+
import json
|
5
|
+
from pathlib import Path
|
2
6
|
from bs4 import BeautifulSoup
|
3
7
|
from janito.tools.adapters.local.adapter import register_local_tool
|
4
8
|
from janito.tools.tool_base import ToolBase, ToolPermissions
|
@@ -15,6 +19,10 @@ class FetchUrlTool(ToolBase):
|
|
15
19
|
Args:
|
16
20
|
url (str): The URL of the web page to fetch.
|
17
21
|
search_strings (list[str], optional): Strings to search for in the page content.
|
22
|
+
max_length (int, optional): Maximum number of characters to return. Defaults to 5000.
|
23
|
+
max_lines (int, optional): Maximum number of lines to return. Defaults to 200.
|
24
|
+
context_chars (int, optional): Characters of context around search matches. Defaults to 400.
|
25
|
+
timeout (int, optional): Timeout in seconds for the HTTP request. Defaults to 10.
|
18
26
|
Returns:
|
19
27
|
str: Extracted text content from the web page, or a warning message. Example:
|
20
28
|
- "<main text content...>"
|
@@ -25,17 +33,101 @@ class FetchUrlTool(ToolBase):
|
|
25
33
|
permissions = ToolPermissions(read=True)
|
26
34
|
tool_name = "fetch_url"
|
27
35
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
self.
|
36
|
+
def __init__(self):
|
37
|
+
super().__init__()
|
38
|
+
self.cache_dir = Path.home() / ".janito" / "cache" / "fetch_url"
|
39
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
40
|
+
self.cache_file = self.cache_dir / "error_cache.json"
|
41
|
+
self._load_cache()
|
42
|
+
|
43
|
+
def _load_cache(self):
|
44
|
+
"""Load error cache from disk."""
|
45
|
+
if self.cache_file.exists():
|
46
|
+
try:
|
47
|
+
with open(self.cache_file, 'r', encoding='utf-8') as f:
|
48
|
+
self.error_cache = json.load(f)
|
49
|
+
except (json.JSONDecodeError, IOError):
|
50
|
+
self.error_cache = {}
|
51
|
+
else:
|
52
|
+
self.error_cache = {}
|
53
|
+
|
54
|
+
def _save_cache(self):
|
55
|
+
"""Save error cache to disk."""
|
56
|
+
try:
|
57
|
+
with open(self.cache_file, 'w', encoding='utf-8') as f:
|
58
|
+
json.dump(self.error_cache, f, indent=2)
|
59
|
+
except IOError:
|
60
|
+
pass # Silently fail if we can't write cache
|
61
|
+
|
62
|
+
def _get_cached_error(self, url: str) -> tuple[str, bool]:
|
63
|
+
"""
|
64
|
+
Check if we have a cached error for this URL.
|
65
|
+
Returns (error_message, is_cached) tuple.
|
66
|
+
"""
|
67
|
+
if url not in self.error_cache:
|
68
|
+
return None, False
|
69
|
+
|
70
|
+
entry = self.error_cache[url]
|
71
|
+
current_time = time.time()
|
72
|
+
|
73
|
+
# Different expiration times for different status codes
|
74
|
+
if entry['status_code'] == 403:
|
75
|
+
# Cache 403 errors for 24 hours (more permanent)
|
76
|
+
expiration_time = 24 * 3600
|
77
|
+
elif entry['status_code'] == 404:
|
78
|
+
# Cache 404 errors for 1 hour (more temporary)
|
79
|
+
expiration_time = 3600
|
80
|
+
else:
|
81
|
+
# Cache other 4xx errors for 30 minutes
|
82
|
+
expiration_time = 1800
|
83
|
+
|
84
|
+
if current_time - entry['timestamp'] > expiration_time:
|
85
|
+
# Cache expired, remove it
|
86
|
+
del self.error_cache[url]
|
87
|
+
self._save_cache()
|
88
|
+
return None, False
|
89
|
+
|
90
|
+
return entry['message'], True
|
91
|
+
|
92
|
+
def _cache_error(self, url: str, status_code: int, message: str):
|
93
|
+
"""Cache an HTTP error response."""
|
94
|
+
self.error_cache[url] = {
|
95
|
+
'status_code': status_code,
|
96
|
+
'message': message,
|
97
|
+
'timestamp': time.time()
|
98
|
+
}
|
99
|
+
self._save_cache()
|
100
|
+
|
101
|
+
def _fetch_url_content(self, url: str, timeout: int = 10) -> str:
|
102
|
+
"""Fetch URL content and handle HTTP errors."""
|
103
|
+
# Check cache first for known errors
|
104
|
+
cached_error, is_cached = self._get_cached_error(url)
|
105
|
+
if cached_error:
|
106
|
+
self.report_warning(
|
107
|
+
tr(
|
108
|
+
"ℹ️ Using cached HTTP error for URL: {url}",
|
109
|
+
url=url,
|
110
|
+
),
|
111
|
+
ReportAction.READ,
|
112
|
+
)
|
113
|
+
return cached_error
|
114
|
+
|
33
115
|
try:
|
34
|
-
response = requests.get(url, timeout=
|
116
|
+
response = requests.get(url, timeout=timeout)
|
35
117
|
response.raise_for_status()
|
118
|
+
return response.text
|
36
119
|
except requests.exceptions.HTTPError as http_err:
|
37
120
|
status_code = http_err.response.status_code if http_err.response else None
|
38
121
|
if status_code and 400 <= status_code < 500:
|
122
|
+
error_message = tr(
|
123
|
+
"Warning: HTTP {status_code} error for URL: {url}",
|
124
|
+
status_code=status_code,
|
125
|
+
url=url,
|
126
|
+
)
|
127
|
+
# Cache 403 and 404 errors
|
128
|
+
if status_code in [403, 404]:
|
129
|
+
self._cache_error(url, status_code, error_message)
|
130
|
+
|
39
131
|
self.report_error(
|
40
132
|
tr(
|
41
133
|
"❗ HTTP {status_code} error for URL: {url}",
|
@@ -44,11 +136,7 @@ class FetchUrlTool(ToolBase):
|
|
44
136
|
),
|
45
137
|
ReportAction.READ,
|
46
138
|
)
|
47
|
-
return
|
48
|
-
"Warning: HTTP {status_code} error for URL: {url}",
|
49
|
-
status_code=status_code,
|
50
|
-
url=url,
|
51
|
-
)
|
139
|
+
return error_message
|
52
140
|
else:
|
53
141
|
self.report_error(
|
54
142
|
tr(
|
@@ -71,27 +159,89 @@ class FetchUrlTool(ToolBase):
|
|
71
159
|
return tr(
|
72
160
|
"Warning: Error fetching URL: {url}: {err}", url=url, err=str(err)
|
73
161
|
)
|
74
|
-
|
162
|
+
|
163
|
+
def _extract_and_clean_text(self, html_content: str) -> str:
|
164
|
+
"""Extract and clean text from HTML content."""
|
165
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
75
166
|
text = soup.get_text(separator="\n")
|
167
|
+
|
168
|
+
# Clean up excessive whitespace
|
169
|
+
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
170
|
+
return "\n".join(lines)
|
171
|
+
|
172
|
+
def _filter_by_search_strings(
|
173
|
+
self, text: str, search_strings: list[str], context_chars: int
|
174
|
+
) -> str:
|
175
|
+
"""Filter text by search strings with context."""
|
176
|
+
filtered = []
|
177
|
+
for s in search_strings:
|
178
|
+
idx = text.find(s)
|
179
|
+
if idx != -1:
|
180
|
+
start = max(0, idx - context_chars)
|
181
|
+
end = min(len(text), idx + len(s) + context_chars)
|
182
|
+
snippet = text[start:end]
|
183
|
+
filtered.append(snippet)
|
184
|
+
|
185
|
+
if filtered:
|
186
|
+
return "\n...\n".join(filtered)
|
187
|
+
else:
|
188
|
+
return tr("No lines found for the provided search strings.")
|
189
|
+
|
190
|
+
def _apply_limits(self, text: str, max_length: int, max_lines: int) -> str:
|
191
|
+
"""Apply length and line limits to text."""
|
192
|
+
# Apply length limit
|
193
|
+
if len(text) > max_length:
|
194
|
+
text = text[:max_length] + "\n... (content truncated due to length limit)"
|
195
|
+
|
196
|
+
# Apply line limit
|
197
|
+
lines = text.splitlines()
|
198
|
+
if len(lines) > max_lines:
|
199
|
+
text = (
|
200
|
+
"\n".join(lines[:max_lines])
|
201
|
+
+ "\n... (content truncated due to line limit)"
|
202
|
+
)
|
203
|
+
|
204
|
+
return text
|
205
|
+
|
206
|
+
def run(
|
207
|
+
self,
|
208
|
+
url: str,
|
209
|
+
search_strings: list[str] = None,
|
210
|
+
max_length: int = 5000,
|
211
|
+
max_lines: int = 200,
|
212
|
+
context_chars: int = 400,
|
213
|
+
timeout: int = 10,
|
214
|
+
) -> str:
|
215
|
+
if not url.strip():
|
216
|
+
self.report_warning(tr("ℹ️ Empty URL provided."), ReportAction.READ)
|
217
|
+
return tr("Warning: Empty URL provided. Operation skipped.")
|
218
|
+
|
219
|
+
self.report_action(tr("🌐 Fetch URL '{url}' ...", url=url), ReportAction.READ)
|
220
|
+
|
221
|
+
# Fetch URL content
|
222
|
+
html_content = self._fetch_url_content(url, timeout=timeout)
|
223
|
+
if html_content.startswith("Warning:"):
|
224
|
+
return html_content
|
225
|
+
|
226
|
+
# Extract and clean text
|
227
|
+
text = self._extract_and_clean_text(html_content)
|
228
|
+
|
229
|
+
# Filter by search strings if provided
|
76
230
|
if search_strings:
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
snippet = text[start:end]
|
84
|
-
filtered.append(snippet)
|
85
|
-
if filtered:
|
86
|
-
text = "\n...\n".join(filtered)
|
87
|
-
else:
|
88
|
-
text = tr("No lines found for the provided search strings.")
|
231
|
+
text = self._filter_by_search_strings(text, search_strings, context_chars)
|
232
|
+
|
233
|
+
# Apply limits
|
234
|
+
text = self._apply_limits(text, max_length, max_lines)
|
235
|
+
|
236
|
+
# Report success
|
89
237
|
num_lines = len(text.splitlines())
|
238
|
+
total_chars = len(text)
|
90
239
|
self.report_success(
|
91
240
|
tr(
|
92
|
-
"✅ {num_lines} {line_word}",
|
241
|
+
"✅ {num_lines} {line_word}, {chars} chars",
|
93
242
|
num_lines=num_lines,
|
94
243
|
line_word=pluralize("line", num_lines),
|
244
|
+
chars=total_chars,
|
95
245
|
),
|
96
246
|
ReportAction.READ,
|
97
247
|
)
|
File without changes
|