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.
Files changed (35) hide show
  1. janito/README.md +47 -4
  2. janito/agent/setup_agent.py +34 -4
  3. janito/agent/templates/profiles/system_prompt_template_Developer_with_Python_Tools.txt.j2 +0 -0
  4. janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +0 -0
  5. janito/agent/templates/profiles/system_prompt_template_market_analyst.txt.j2 +10 -0
  6. janito/agent/templates/profiles/system_prompt_template_model_conversation_without_tools_or_context.txt.j2 +0 -0
  7. janito/cli/chat_mode/session_profile_select.py +20 -3
  8. janito/cli/chat_mode/shell/commands.bak.zip +0 -0
  9. janito/cli/chat_mode/shell/session.bak.zip +0 -0
  10. janito/cli/cli_commands/list_profiles.py +29 -1
  11. janito/cli/cli_commands/show_system_prompt.py +45 -4
  12. janito/docs/GETTING_STARTED.md +85 -12
  13. janito/drivers/dashscope.bak.zip +0 -0
  14. janito/drivers/openai/README.md +0 -0
  15. janito/drivers/openai_responses.bak.zip +0 -0
  16. janito/llm/README.md +0 -0
  17. janito/mkdocs.yml +0 -0
  18. janito/providers/__init__.py +1 -0
  19. janito/providers/azure_openai/provider.py +1 -1
  20. janito/providers/dashscope.bak.zip +0 -0
  21. janito/providers/ibm/README.md +99 -0
  22. janito/providers/ibm/__init__.py +1 -0
  23. janito/providers/ibm/model_info.py +87 -0
  24. janito/providers/ibm/provider.py +149 -0
  25. janito/shell.bak.zip +0 -0
  26. janito/tools/DOCSTRING_STANDARD.txt +0 -0
  27. janito/tools/README.md +0 -0
  28. janito/tools/adapters/local/fetch_url.py +175 -25
  29. janito/tools/outline_file.bak.zip +0 -0
  30. {janito-2.20.1.dist-info → janito-2.22.0.dist-info}/METADATA +411 -411
  31. {janito-2.20.1.dist-info → janito-2.22.0.dist-info}/RECORD +20 -15
  32. {janito-2.20.1.dist-info → janito-2.22.0.dist-info}/entry_points.txt +0 -0
  33. {janito-2.20.1.dist-info → janito-2.22.0.dist-info}/licenses/LICENSE +0 -0
  34. {janito-2.20.1.dist-info → janito-2.22.0.dist-info}/top_level.txt +0 -0
  35. {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 run(self, url: str, search_strings: list[str] = None) -> str:
29
- if not url.strip():
30
- self.report_warning(tr("ℹ️ Empty URL provided."), ReportAction.READ)
31
- return tr("Warning: Empty URL provided. Operation skipped.")
32
- self.report_action(tr("🌐 Fetch URL '{url}' ...", url=url), ReportAction.READ)
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=10)
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 tr(
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
- soup = BeautifulSoup(response.text, "html.parser")
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
- filtered = []
78
- for s in search_strings:
79
- idx = text.find(s)
80
- if idx != -1:
81
- start = max(0, idx - 200)
82
- end = min(len(text), idx + len(s) + 200)
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