janito 3.12.0__py3-none-any.whl → 3.12.2__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 +191 -191
- janito/_version.py +55 -55
- janito/agent/setup_agent.py +378 -377
- janito/cli/chat_mode/session.py +505 -505
- janito/cli/cli_commands/list_drivers.py +162 -162
- janito/cli/cli_commands/list_profiles.py +104 -107
- janito/cli/cli_commands/show_system_prompt.py +166 -166
- janito/cli/console.py +3 -3
- janito/cli/core/runner.py +250 -266
- janito/cli/main_cli.py +520 -519
- janito/cli/rich_terminal_reporter.py +2 -4
- janito/cli/single_shot_mode/handler.py +167 -167
- janito/docs/GETTING_STARTED.md +189 -189
- janito/llm/__init__.py +6 -5
- janito/llm/driver.py +290 -254
- janito/llm/response_cache.py +57 -0
- janito/plugins/builtin.py +64 -88
- janito/plugins/discovery.py +289 -289
- janito/plugins/tools/local/__init__.py +82 -80
- janito/plugins/tools/local/markdown_view.py +94 -0
- janito/plugins/tools/local/show_image.py +119 -74
- janito/plugins/tools/local/show_image_grid.py +134 -76
- janito/providers/alibaba/model_info.py +136 -105
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/METADATA +5 -4
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/RECORD +29 -28
- janito/mkdocs.yml +0 -40
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/WHEEL +0 -0
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/entry_points.txt +0 -0
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/licenses/LICENSE +0 -0
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/top_level.txt +0 -0
janito/cli/chat_mode/session.py
CHANGED
@@ -1,505 +1,505 @@
|
|
1
|
-
"""
|
2
|
-
Session management for Janito Chat CLI.
|
3
|
-
Defines ChatSession and ChatShellState classes.
|
4
|
-
"""
|
5
|
-
|
6
|
-
from __future__ import annotations
|
7
|
-
|
8
|
-
import types
|
9
|
-
from rich.console import Console
|
10
|
-
from rich.rule import Rule
|
11
|
-
from prompt_toolkit.history import InMemoryHistory
|
12
|
-
from janito.cli.chat_mode.shell.input_history import UserInputHistory
|
13
|
-
from prompt_toolkit.formatted_text import HTML
|
14
|
-
from prompt_toolkit import PromptSession
|
15
|
-
from janito.cli.chat_mode.toolbar import get_toolbar_func
|
16
|
-
from prompt_toolkit.enums import EditingMode
|
17
|
-
from janito.cli.chat_mode.prompt_style import chat_shell_style
|
18
|
-
from janito.cli.chat_mode.bindings import KeyBindingsFactory
|
19
|
-
from janito.cli.chat_mode.shell.commands import handle_command
|
20
|
-
from janito.cli.chat_mode.shell.autocomplete import ShellCommandCompleter
|
21
|
-
import time
|
22
|
-
|
23
|
-
# Shared prompt/agent factory
|
24
|
-
from janito.cli.prompt_setup import setup_agent_and_prompt_handler
|
25
|
-
|
26
|
-
import time
|
27
|
-
|
28
|
-
|
29
|
-
class ChatShellState:
|
30
|
-
def __init__(self, mem_history, conversation_history):
|
31
|
-
self.mem_history = mem_history
|
32
|
-
self.conversation_history = conversation_history
|
33
|
-
self.paste_mode = False
|
34
|
-
self.interactive_mode = True # Default to interactive mode
|
35
|
-
|
36
|
-
self._pid = None
|
37
|
-
self._stdout_path = None
|
38
|
-
self._stderr_path = None
|
39
|
-
|
40
|
-
self._status = (
|
41
|
-
"starting" # Tracks the current status (updated by background thread/UI)
|
42
|
-
)
|
43
|
-
|
44
|
-
self.last_usage_info = {}
|
45
|
-
self.last_elapsed = None
|
46
|
-
self.main_agent = {}
|
47
|
-
self.mode = None
|
48
|
-
self.agent = None
|
49
|
-
self.main_agent = None
|
50
|
-
self.main_enabled = False
|
51
|
-
self.no_tools_mode = False
|
52
|
-
|
53
|
-
|
54
|
-
class ChatSession:
|
55
|
-
def __init__(
|
56
|
-
self,
|
57
|
-
console,
|
58
|
-
provider_instance=None,
|
59
|
-
llm_driver_config=None,
|
60
|
-
role=None,
|
61
|
-
args=None,
|
62
|
-
verbose_tools=False,
|
63
|
-
verbose_agent=False,
|
64
|
-
allowed_permissions=None,
|
65
|
-
):
|
66
|
-
self.console = console
|
67
|
-
self.session_start_time = time.time()
|
68
|
-
self.user_input_history = UserInputHistory()
|
69
|
-
self.input_dicts = self.user_input_history.load()
|
70
|
-
self.mem_history = InMemoryHistory()
|
71
|
-
for item in self.input_dicts:
|
72
|
-
if isinstance(item, dict) and "input" in item:
|
73
|
-
self.mem_history.append_string(item["input"])
|
74
|
-
self.provider_instance = provider_instance
|
75
|
-
self.llm_driver_config = llm_driver_config
|
76
|
-
|
77
|
-
profile, role, profile_system_prompt, no_tools_mode = (
|
78
|
-
self._select_profile_and_role(args, role)
|
79
|
-
)
|
80
|
-
# Propagate no_tools_mode flag to downstream components via args
|
81
|
-
if args is not None and not hasattr(args, "no_tools_mode"):
|
82
|
-
try:
|
83
|
-
setattr(args, "no_tools_mode", no_tools_mode)
|
84
|
-
except Exception:
|
85
|
-
pass
|
86
|
-
conversation_history = self._create_conversation_history()
|
87
|
-
self.agent, self._prompt_handler = self._setup_agent_and_prompt_handler(
|
88
|
-
args,
|
89
|
-
provider_instance,
|
90
|
-
llm_driver_config,
|
91
|
-
role,
|
92
|
-
verbose_tools,
|
93
|
-
verbose_agent,
|
94
|
-
allowed_permissions,
|
95
|
-
profile,
|
96
|
-
profile_system_prompt,
|
97
|
-
conversation_history,
|
98
|
-
)
|
99
|
-
self.profile = profile # Store profile name for welcome message
|
100
|
-
self.shell_state = ChatShellState(self.mem_history, conversation_history)
|
101
|
-
self.shell_state.agent = self.agent
|
102
|
-
# Set no_tools_mode if present
|
103
|
-
self.shell_state.no_tools_mode = bool(no_tools_mode)
|
104
|
-
self._filter_execution_tools()
|
105
|
-
from janito.perf_singleton import performance_collector
|
106
|
-
|
107
|
-
self.performance_collector = performance_collector
|
108
|
-
self.key_bindings = KeyBindingsFactory.create()
|
109
|
-
self._prompt_handler.agent = self.agent
|
110
|
-
self._prompt_handler.conversation_history = (
|
111
|
-
self.shell_state.conversation_history
|
112
|
-
)
|
113
|
-
self._support = False
|
114
|
-
|
115
|
-
# Check if multi-line mode should be enabled by default
|
116
|
-
self.multi_line_mode = getattr(args, "multi", False) if args else False
|
117
|
-
|
118
|
-
def _select_profile_and_role(self, args, role):
|
119
|
-
profile, role_arg, python_profile, market_profile = self._extract_args(args)
|
120
|
-
profile_system_prompt = None
|
121
|
-
no_tools_mode = False
|
122
|
-
|
123
|
-
profile = self._determine_profile(profile, python_profile, market_profile)
|
124
|
-
|
125
|
-
if (
|
126
|
-
profile is None
|
127
|
-
and role_arg is None
|
128
|
-
and not python_profile
|
129
|
-
and not market_profile
|
130
|
-
):
|
131
|
-
skip_profile_selection = self._should_skip_profile_selection(args)
|
132
|
-
else:
|
133
|
-
skip_profile_selection = False
|
134
|
-
|
135
|
-
if skip_profile_selection:
|
136
|
-
profile = "Developer
|
137
|
-
else:
|
138
|
-
profile = "Developer
|
139
|
-
|
140
|
-
return profile, role, profile_system_prompt, no_tools_mode
|
141
|
-
|
142
|
-
def _create_conversation_history(self):
|
143
|
-
from janito.conversation_history import LLMConversationHistory
|
144
|
-
|
145
|
-
return LLMConversationHistory()
|
146
|
-
|
147
|
-
def _setup_agent_and_prompt_handler(
|
148
|
-
self,
|
149
|
-
args,
|
150
|
-
provider_instance,
|
151
|
-
llm_driver_config,
|
152
|
-
role,
|
153
|
-
verbose_tools,
|
154
|
-
verbose_agent,
|
155
|
-
allowed_permissions,
|
156
|
-
profile,
|
157
|
-
profile_system_prompt,
|
158
|
-
conversation_history,
|
159
|
-
):
|
160
|
-
return setup_agent_and_prompt_handler(
|
161
|
-
args=args,
|
162
|
-
provider_instance=provider_instance,
|
163
|
-
llm_driver_config=llm_driver_config,
|
164
|
-
role=role,
|
165
|
-
verbose_tools=verbose_tools,
|
166
|
-
verbose_agent=verbose_agent,
|
167
|
-
allowed_permissions=allowed_permissions,
|
168
|
-
profile=profile,
|
169
|
-
profile_system_prompt=profile_system_prompt,
|
170
|
-
conversation_history=conversation_history,
|
171
|
-
)
|
172
|
-
|
173
|
-
def _filter_execution_tools(self):
|
174
|
-
try:
|
175
|
-
getattr(
|
176
|
-
__import__("janito.tools", fromlist=["get_local_tools_adapter"]),
|
177
|
-
"get_local_tools_adapter",
|
178
|
-
)()
|
179
|
-
except Exception as e:
|
180
|
-
self.console.print(
|
181
|
-
f"[yellow]Warning: Could not filter execution tools at startup: {e}[/yellow]"
|
182
|
-
)
|
183
|
-
|
184
|
-
_thread = _start_and_watch(self.shell_state, self._lock, get__port())
|
185
|
-
self._thread = _thread
|
186
|
-
else:
|
187
|
-
self.shell_state._support = False
|
188
|
-
self.shell_state._status = "offline"
|
189
|
-
|
190
|
-
def run(self):
|
191
|
-
self.console.clear()
|
192
|
-
from janito import __version__
|
193
|
-
|
194
|
-
self.console.print(f"[bold green]Janito Chat Mode v{__version__}[/bold green]")
|
195
|
-
self.console.print(f"[dim]Profile: {self.profile}[/dim]")
|
196
|
-
|
197
|
-
import os
|
198
|
-
|
199
|
-
cwd = os.getcwd()
|
200
|
-
home = os.path.expanduser("~")
|
201
|
-
if cwd.startswith(home):
|
202
|
-
cwd_display = "~" + cwd[len(home) :]
|
203
|
-
else:
|
204
|
-
cwd_display = cwd
|
205
|
-
from janito.cli.chat_mode.shell.commands._priv_status import (
|
206
|
-
get_privilege_status_message,
|
207
|
-
)
|
208
|
-
|
209
|
-
priv_status = get_privilege_status_message()
|
210
|
-
self.console.print(
|
211
|
-
f"[green]Working Dir:[/green] [cyan]{cwd_display}[/cyan] | {priv_status}"
|
212
|
-
)
|
213
|
-
|
214
|
-
if self.multi_line_mode:
|
215
|
-
self.console.print(
|
216
|
-
"[blue]Multi-line input mode enabled (Esc+Enter or Ctrl+D to submit)[/blue]"
|
217
|
-
)
|
218
|
-
|
219
|
-
from janito.cli.chat_mode.shell.commands._priv_check import (
|
220
|
-
user_has_any_privileges,
|
221
|
-
)
|
222
|
-
|
223
|
-
perms = __import__(
|
224
|
-
"janito.tools.permissions", fromlist=["get_global_allowed_permissions"]
|
225
|
-
).get_global_allowed_permissions()
|
226
|
-
if perms.execute:
|
227
|
-
self.console.print(
|
228
|
-
"[bold red]Commands/Code execution is enabled - Be cautious[/bold red]"
|
229
|
-
)
|
230
|
-
if not (perms.read or perms.write or perms.execute):
|
231
|
-
self.console.print(
|
232
|
-
"[yellow]Note: You currently have no privileges enabled. If you need to interact with files or the system, enable permissions using /read on, /write on, or /execute on.[/yellow]"
|
233
|
-
)
|
234
|
-
|
235
|
-
session = self._create_prompt_session()
|
236
|
-
self._chat_loop(session)
|
237
|
-
|
238
|
-
def _chat_loop(self, session):
|
239
|
-
self.msg_count = 0
|
240
|
-
timer_started = False
|
241
|
-
while True:
|
242
|
-
if not timer_started:
|
243
|
-
timer_started = True
|
244
|
-
cmd_input = self._handle_input(session)
|
245
|
-
if cmd_input is None:
|
246
|
-
break
|
247
|
-
if not cmd_input:
|
248
|
-
continue
|
249
|
-
if self._handle_exit_conditions(cmd_input):
|
250
|
-
break
|
251
|
-
if self._handle_command_input(cmd_input):
|
252
|
-
continue
|
253
|
-
self.user_input_history.append(cmd_input)
|
254
|
-
self._process_prompt(cmd_input)
|
255
|
-
|
256
|
-
def _handle_command_input(self, cmd_input):
|
257
|
-
if cmd_input.startswith("/"):
|
258
|
-
handle_command(cmd_input, shell_state=self.shell_state)
|
259
|
-
return True
|
260
|
-
if cmd_input.startswith("!"):
|
261
|
-
handle_command(f"! {cmd_input[1:]}", shell_state=self.shell_state)
|
262
|
-
return True
|
263
|
-
return False
|
264
|
-
|
265
|
-
def _process_prompt(self, cmd_input):
|
266
|
-
try:
|
267
|
-
# Clear screen before processing new prompt
|
268
|
-
self.console.clear()
|
269
|
-
import time
|
270
|
-
|
271
|
-
final_event = (
|
272
|
-
self._prompt_handler.agent.last_event
|
273
|
-
if hasattr(self._prompt_handler.agent, "last_event")
|
274
|
-
else None
|
275
|
-
)
|
276
|
-
start_time = time.time()
|
277
|
-
|
278
|
-
model_name, provider_name = self._get_model_info()
|
279
|
-
backend_hostname = self._get_backend_hostname()
|
280
|
-
|
281
|
-
self.console.print(
|
282
|
-
Rule(
|
283
|
-
f"[bold blue]Model: {model_name} ({provider_name}) | Backend: {backend_hostname}[/bold blue]"
|
284
|
-
)
|
285
|
-
)
|
286
|
-
|
287
|
-
self._prompt_handler.run_prompt(cmd_input)
|
288
|
-
end_time = time.time()
|
289
|
-
elapsed = end_time - start_time
|
290
|
-
self.msg_count += 1
|
291
|
-
from janito.formatting_token import print_token_message_summary
|
292
|
-
|
293
|
-
usage = self.performance_collector.get_last_request_usage()
|
294
|
-
print_token_message_summary(
|
295
|
-
self.console, self.msg_count, usage, elapsed=elapsed
|
296
|
-
)
|
297
|
-
if final_event and hasattr(final_event, "metadata"):
|
298
|
-
exit_reason = (
|
299
|
-
final_event.metadata.get("exit_reason")
|
300
|
-
if hasattr(final_event, "metadata")
|
301
|
-
else None
|
302
|
-
)
|
303
|
-
if exit_reason:
|
304
|
-
self.console.print(
|
305
|
-
f"[bold yellow]Exit reason: {exit_reason}[/bold yellow]"
|
306
|
-
)
|
307
|
-
except Exception as exc:
|
308
|
-
self.console.print(f"[red]Exception in agent: {exc}[/red]")
|
309
|
-
import traceback
|
310
|
-
|
311
|
-
self.console.print(traceback.format_exc())
|
312
|
-
|
313
|
-
def _extract_args(self, args):
|
314
|
-
"""Extract profile and role arguments from args."""
|
315
|
-
profile = getattr(args, "profile", None) if args is not None else None
|
316
|
-
role_arg = None
|
317
|
-
python_profile = (
|
318
|
-
getattr(args, "developer", False) if args is not None else False
|
319
|
-
)
|
320
|
-
market_profile = getattr(args, "market", False) if args is not None else False
|
321
|
-
return profile, role_arg, python_profile, market_profile
|
322
|
-
|
323
|
-
def _determine_profile(self, profile, python_profile, market_profile):
|
324
|
-
"""Determine the profile based on flags and arguments."""
|
325
|
-
if python_profile and profile is None:
|
326
|
-
return "Developer
|
327
|
-
if market_profile and profile is None:
|
328
|
-
return "Market Analyst"
|
329
|
-
return profile
|
330
|
-
|
331
|
-
def _should_skip_profile_selection(self, args):
|
332
|
-
"""Check if profile selection should be skipped for getter commands."""
|
333
|
-
from janito.cli.core.getters import GETTER_KEYS
|
334
|
-
|
335
|
-
if args is None:
|
336
|
-
return False
|
337
|
-
|
338
|
-
for key in GETTER_KEYS:
|
339
|
-
if getattr(args, key, False):
|
340
|
-
return True
|
341
|
-
return False
|
342
|
-
|
343
|
-
def _get_model_info(self):
|
344
|
-
"""Get model and provider information."""
|
345
|
-
model_name = (
|
346
|
-
self.agent.get_model_name()
|
347
|
-
if hasattr(self.agent, "get_model_name")
|
348
|
-
else "Unknown"
|
349
|
-
)
|
350
|
-
provider_name = (
|
351
|
-
self.agent.get_provider_name()
|
352
|
-
if hasattr(self.agent, "get_provider_name")
|
353
|
-
else "Unknown"
|
354
|
-
)
|
355
|
-
return model_name, provider_name
|
356
|
-
|
357
|
-
def _get_backend_hostname(self):
|
358
|
-
"""Extract backend hostname from agent configuration."""
|
359
|
-
candidates = self._collect_base_urls()
|
360
|
-
return self._parse_hostname_from_urls(candidates)
|
361
|
-
|
362
|
-
def _collect_base_urls(self):
|
363
|
-
"""Collect all possible base URLs from agent configuration."""
|
364
|
-
candidates = []
|
365
|
-
|
366
|
-
# Collect from driver
|
367
|
-
drv = getattr(self.agent, "driver", None)
|
368
|
-
if drv is not None:
|
369
|
-
cfg = getattr(drv, "config", None)
|
370
|
-
if cfg is not None:
|
371
|
-
b = getattr(cfg, "base_url", None)
|
372
|
-
if b:
|
373
|
-
candidates.append(b)
|
374
|
-
direct_base = getattr(drv, "base_url", None)
|
375
|
-
if direct_base:
|
376
|
-
candidates.append(direct_base)
|
377
|
-
|
378
|
-
# Collect from agent config
|
379
|
-
cfg2 = getattr(self.agent, "config", None)
|
380
|
-
if cfg2 is not None:
|
381
|
-
b2 = getattr(cfg2, "base_url", None)
|
382
|
-
if b2:
|
383
|
-
candidates.append(b2)
|
384
|
-
|
385
|
-
# Collect from agent directly
|
386
|
-
top_base = getattr(self.agent, "base_url", None)
|
387
|
-
if top_base:
|
388
|
-
candidates.append(top_base)
|
389
|
-
|
390
|
-
return candidates
|
391
|
-
|
392
|
-
def _parse_hostname_from_urls(self, candidates):
|
393
|
-
"""Parse hostname from a list of URL candidates."""
|
394
|
-
from urllib.parse import urlparse
|
395
|
-
|
396
|
-
for candidate in candidates:
|
397
|
-
try:
|
398
|
-
if not candidate:
|
399
|
-
continue
|
400
|
-
parsed = urlparse(str(candidate))
|
401
|
-
host = parsed.netloc or parsed.path
|
402
|
-
if host:
|
403
|
-
return host
|
404
|
-
except Exception:
|
405
|
-
return str(candidate)
|
406
|
-
|
407
|
-
return "Unknown"
|
408
|
-
|
409
|
-
def _create_prompt_session(self):
|
410
|
-
return PromptSession(
|
411
|
-
style=chat_shell_style,
|
412
|
-
completer=ShellCommandCompleter(),
|
413
|
-
history=self.mem_history,
|
414
|
-
editing_mode=EditingMode.EMACS,
|
415
|
-
key_bindings=self.key_bindings,
|
416
|
-
bottom_toolbar=lambda: get_toolbar_func(
|
417
|
-
self.performance_collector, 0, self.shell_state
|
418
|
-
)(),
|
419
|
-
multiline=self.multi_line_mode,
|
420
|
-
)
|
421
|
-
|
422
|
-
def _handle_input(self, session):
|
423
|
-
injected = getattr(self.shell_state, "injected_input", None)
|
424
|
-
if injected is not None:
|
425
|
-
cmd_input = injected
|
426
|
-
self.shell_state.injected_input = None
|
427
|
-
else:
|
428
|
-
try:
|
429
|
-
cmd_input = session.prompt(HTML("<inputline>💬 </inputline>"))
|
430
|
-
except KeyboardInterrupt:
|
431
|
-
# Ask for confirmation on Ctrl+C
|
432
|
-
from prompt_toolkit import prompt
|
433
|
-
|
434
|
-
try:
|
435
|
-
confirm = prompt(
|
436
|
-
"Are you sure you want to exit? (y/n): ",
|
437
|
-
style=self._create_prompt_session().style,
|
438
|
-
)
|
439
|
-
if confirm.lower() == "y":
|
440
|
-
self._handle_exit()
|
441
|
-
return None
|
442
|
-
else:
|
443
|
-
return "" # Return empty string to continue
|
444
|
-
except (KeyboardInterrupt, EOFError):
|
445
|
-
# Handle second Ctrl+C or Ctrl+D as immediate exit
|
446
|
-
self._handle_exit()
|
447
|
-
return None
|
448
|
-
except EOFError:
|
449
|
-
self._handle_exit()
|
450
|
-
return None
|
451
|
-
sanitized = cmd_input.strip()
|
452
|
-
try:
|
453
|
-
sanitized.encode("utf-8")
|
454
|
-
except UnicodeEncodeError:
|
455
|
-
sanitized = sanitized.encode("utf-8", errors="replace").decode("utf-8")
|
456
|
-
self.console.print(
|
457
|
-
"[yellow]Warning: Some characters in your input were not valid UTF-8 and have been replaced.[/yellow]"
|
458
|
-
)
|
459
|
-
return sanitized
|
460
|
-
|
461
|
-
def _handle_exit(self):
|
462
|
-
session_duration = time.time() - self.session_start_time
|
463
|
-
|
464
|
-
# Get total token usage from performance collector
|
465
|
-
from janito.perf_singleton import performance_collector
|
466
|
-
|
467
|
-
total_tokens = performance_collector.get_token_usage().get("total_tokens", 0)
|
468
|
-
|
469
|
-
# Format session duration
|
470
|
-
if session_duration < 60:
|
471
|
-
duration_str = f"{session_duration:.1f}s"
|
472
|
-
elif session_duration < 3600:
|
473
|
-
duration_str = f"{session_duration/60:.1f}m"
|
474
|
-
else:
|
475
|
-
duration_str = f"{session_duration/3600:.1f}h"
|
476
|
-
|
477
|
-
# Format tokens in k/m/t as appropriate
|
478
|
-
if total_tokens >= 1_000_000_000:
|
479
|
-
token_str = f"{total_tokens/1_000_000_000:.1f}t"
|
480
|
-
elif total_tokens >= 1_000_000:
|
481
|
-
token_str = f"{total_tokens/1_000_000:.1f}m"
|
482
|
-
elif total_tokens >= 1_000:
|
483
|
-
token_str = f"{total_tokens/1_000:.1f}k"
|
484
|
-
else:
|
485
|
-
token_str = f"{total_tokens}"
|
486
|
-
|
487
|
-
self.console.print(f"[bold yellow]Session completed![/bold yellow]")
|
488
|
-
self.console.print(
|
489
|
-
f"[dim]Session time: {duration_str} | Total tokens: {token_str}[/dim]"
|
490
|
-
)
|
491
|
-
self.console.print("[bold yellow]Goodbye![/bold yellow]")
|
492
|
-
|
493
|
-
if hasattr(self, "agent") and hasattr(self.agent, "join_driver"):
|
494
|
-
if (
|
495
|
-
hasattr(self.agent, "input_queue")
|
496
|
-
and self.agent.input_queue is not None
|
497
|
-
):
|
498
|
-
self.agent.input_queue.put(None)
|
499
|
-
self.agent.join_driver()
|
500
|
-
|
501
|
-
def _handle_exit_conditions(self, cmd_input):
|
502
|
-
if cmd_input.lower() in ("/exit", ":q", ":quit"):
|
503
|
-
self._handle_exit()
|
504
|
-
return True
|
505
|
-
return False
|
1
|
+
"""
|
2
|
+
Session management for Janito Chat CLI.
|
3
|
+
Defines ChatSession and ChatShellState classes.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import types
|
9
|
+
from rich.console import Console
|
10
|
+
from rich.rule import Rule
|
11
|
+
from prompt_toolkit.history import InMemoryHistory
|
12
|
+
from janito.cli.chat_mode.shell.input_history import UserInputHistory
|
13
|
+
from prompt_toolkit.formatted_text import HTML
|
14
|
+
from prompt_toolkit import PromptSession
|
15
|
+
from janito.cli.chat_mode.toolbar import get_toolbar_func
|
16
|
+
from prompt_toolkit.enums import EditingMode
|
17
|
+
from janito.cli.chat_mode.prompt_style import chat_shell_style
|
18
|
+
from janito.cli.chat_mode.bindings import KeyBindingsFactory
|
19
|
+
from janito.cli.chat_mode.shell.commands import handle_command
|
20
|
+
from janito.cli.chat_mode.shell.autocomplete import ShellCommandCompleter
|
21
|
+
import time
|
22
|
+
|
23
|
+
# Shared prompt/agent factory
|
24
|
+
from janito.cli.prompt_setup import setup_agent_and_prompt_handler
|
25
|
+
|
26
|
+
import time
|
27
|
+
|
28
|
+
|
29
|
+
class ChatShellState:
|
30
|
+
def __init__(self, mem_history, conversation_history):
|
31
|
+
self.mem_history = mem_history
|
32
|
+
self.conversation_history = conversation_history
|
33
|
+
self.paste_mode = False
|
34
|
+
self.interactive_mode = True # Default to interactive mode
|
35
|
+
|
36
|
+
self._pid = None
|
37
|
+
self._stdout_path = None
|
38
|
+
self._stderr_path = None
|
39
|
+
|
40
|
+
self._status = (
|
41
|
+
"starting" # Tracks the current status (updated by background thread/UI)
|
42
|
+
)
|
43
|
+
|
44
|
+
self.last_usage_info = {}
|
45
|
+
self.last_elapsed = None
|
46
|
+
self.main_agent = {}
|
47
|
+
self.mode = None
|
48
|
+
self.agent = None
|
49
|
+
self.main_agent = None
|
50
|
+
self.main_enabled = False
|
51
|
+
self.no_tools_mode = False
|
52
|
+
|
53
|
+
|
54
|
+
class ChatSession:
|
55
|
+
def __init__(
|
56
|
+
self,
|
57
|
+
console,
|
58
|
+
provider_instance=None,
|
59
|
+
llm_driver_config=None,
|
60
|
+
role=None,
|
61
|
+
args=None,
|
62
|
+
verbose_tools=False,
|
63
|
+
verbose_agent=False,
|
64
|
+
allowed_permissions=None,
|
65
|
+
):
|
66
|
+
self.console = console
|
67
|
+
self.session_start_time = time.time()
|
68
|
+
self.user_input_history = UserInputHistory()
|
69
|
+
self.input_dicts = self.user_input_history.load()
|
70
|
+
self.mem_history = InMemoryHistory()
|
71
|
+
for item in self.input_dicts:
|
72
|
+
if isinstance(item, dict) and "input" in item:
|
73
|
+
self.mem_history.append_string(item["input"])
|
74
|
+
self.provider_instance = provider_instance
|
75
|
+
self.llm_driver_config = llm_driver_config
|
76
|
+
|
77
|
+
profile, role, profile_system_prompt, no_tools_mode = (
|
78
|
+
self._select_profile_and_role(args, role)
|
79
|
+
)
|
80
|
+
# Propagate no_tools_mode flag to downstream components via args
|
81
|
+
if args is not None and not hasattr(args, "no_tools_mode"):
|
82
|
+
try:
|
83
|
+
setattr(args, "no_tools_mode", no_tools_mode)
|
84
|
+
except Exception:
|
85
|
+
pass
|
86
|
+
conversation_history = self._create_conversation_history()
|
87
|
+
self.agent, self._prompt_handler = self._setup_agent_and_prompt_handler(
|
88
|
+
args,
|
89
|
+
provider_instance,
|
90
|
+
llm_driver_config,
|
91
|
+
role,
|
92
|
+
verbose_tools,
|
93
|
+
verbose_agent,
|
94
|
+
allowed_permissions,
|
95
|
+
profile,
|
96
|
+
profile_system_prompt,
|
97
|
+
conversation_history,
|
98
|
+
)
|
99
|
+
self.profile = profile # Store profile name for welcome message
|
100
|
+
self.shell_state = ChatShellState(self.mem_history, conversation_history)
|
101
|
+
self.shell_state.agent = self.agent
|
102
|
+
# Set no_tools_mode if present
|
103
|
+
self.shell_state.no_tools_mode = bool(no_tools_mode)
|
104
|
+
self._filter_execution_tools()
|
105
|
+
from janito.perf_singleton import performance_collector
|
106
|
+
|
107
|
+
self.performance_collector = performance_collector
|
108
|
+
self.key_bindings = KeyBindingsFactory.create()
|
109
|
+
self._prompt_handler.agent = self.agent
|
110
|
+
self._prompt_handler.conversation_history = (
|
111
|
+
self.shell_state.conversation_history
|
112
|
+
)
|
113
|
+
self._support = False
|
114
|
+
|
115
|
+
# Check if multi-line mode should be enabled by default
|
116
|
+
self.multi_line_mode = getattr(args, "multi", False) if args else False
|
117
|
+
|
118
|
+
def _select_profile_and_role(self, args, role):
|
119
|
+
profile, role_arg, python_profile, market_profile = self._extract_args(args)
|
120
|
+
profile_system_prompt = None
|
121
|
+
no_tools_mode = False
|
122
|
+
|
123
|
+
profile = self._determine_profile(profile, python_profile, market_profile)
|
124
|
+
|
125
|
+
if (
|
126
|
+
profile is None
|
127
|
+
and role_arg is None
|
128
|
+
and not python_profile
|
129
|
+
and not market_profile
|
130
|
+
):
|
131
|
+
skip_profile_selection = self._should_skip_profile_selection(args)
|
132
|
+
else:
|
133
|
+
skip_profile_selection = False
|
134
|
+
|
135
|
+
if skip_profile_selection:
|
136
|
+
profile = "Developer" # Default for non-interactive commands
|
137
|
+
else:
|
138
|
+
profile = "Developer"
|
139
|
+
|
140
|
+
return profile, role, profile_system_prompt, no_tools_mode
|
141
|
+
|
142
|
+
def _create_conversation_history(self):
|
143
|
+
from janito.conversation_history import LLMConversationHistory
|
144
|
+
|
145
|
+
return LLMConversationHistory()
|
146
|
+
|
147
|
+
def _setup_agent_and_prompt_handler(
|
148
|
+
self,
|
149
|
+
args,
|
150
|
+
provider_instance,
|
151
|
+
llm_driver_config,
|
152
|
+
role,
|
153
|
+
verbose_tools,
|
154
|
+
verbose_agent,
|
155
|
+
allowed_permissions,
|
156
|
+
profile,
|
157
|
+
profile_system_prompt,
|
158
|
+
conversation_history,
|
159
|
+
):
|
160
|
+
return setup_agent_and_prompt_handler(
|
161
|
+
args=args,
|
162
|
+
provider_instance=provider_instance,
|
163
|
+
llm_driver_config=llm_driver_config,
|
164
|
+
role=role,
|
165
|
+
verbose_tools=verbose_tools,
|
166
|
+
verbose_agent=verbose_agent,
|
167
|
+
allowed_permissions=allowed_permissions,
|
168
|
+
profile=profile,
|
169
|
+
profile_system_prompt=profile_system_prompt,
|
170
|
+
conversation_history=conversation_history,
|
171
|
+
)
|
172
|
+
|
173
|
+
def _filter_execution_tools(self):
|
174
|
+
try:
|
175
|
+
getattr(
|
176
|
+
__import__("janito.tools", fromlist=["get_local_tools_adapter"]),
|
177
|
+
"get_local_tools_adapter",
|
178
|
+
)()
|
179
|
+
except Exception as e:
|
180
|
+
self.console.print(
|
181
|
+
f"[yellow]Warning: Could not filter execution tools at startup: {e}[/yellow]"
|
182
|
+
)
|
183
|
+
|
184
|
+
_thread = _start_and_watch(self.shell_state, self._lock, get__port())
|
185
|
+
self._thread = _thread
|
186
|
+
else:
|
187
|
+
self.shell_state._support = False
|
188
|
+
self.shell_state._status = "offline"
|
189
|
+
|
190
|
+
def run(self):
|
191
|
+
self.console.clear()
|
192
|
+
from janito import __version__
|
193
|
+
|
194
|
+
self.console.print(f"[bold green]Janito Chat Mode v{__version__}[/bold green]")
|
195
|
+
self.console.print(f"[dim]Profile: {self.profile}[/dim]")
|
196
|
+
|
197
|
+
import os
|
198
|
+
|
199
|
+
cwd = os.getcwd()
|
200
|
+
home = os.path.expanduser("~")
|
201
|
+
if cwd.startswith(home):
|
202
|
+
cwd_display = "~" + cwd[len(home) :]
|
203
|
+
else:
|
204
|
+
cwd_display = cwd
|
205
|
+
from janito.cli.chat_mode.shell.commands._priv_status import (
|
206
|
+
get_privilege_status_message,
|
207
|
+
)
|
208
|
+
|
209
|
+
priv_status = get_privilege_status_message()
|
210
|
+
self.console.print(
|
211
|
+
f"[green]Working Dir:[/green] [cyan]{cwd_display}[/cyan] | {priv_status}"
|
212
|
+
)
|
213
|
+
|
214
|
+
if self.multi_line_mode:
|
215
|
+
self.console.print(
|
216
|
+
"[blue]Multi-line input mode enabled (Esc+Enter or Ctrl+D to submit)[/blue]"
|
217
|
+
)
|
218
|
+
|
219
|
+
from janito.cli.chat_mode.shell.commands._priv_check import (
|
220
|
+
user_has_any_privileges,
|
221
|
+
)
|
222
|
+
|
223
|
+
perms = __import__(
|
224
|
+
"janito.tools.permissions", fromlist=["get_global_allowed_permissions"]
|
225
|
+
).get_global_allowed_permissions()
|
226
|
+
if perms.execute:
|
227
|
+
self.console.print(
|
228
|
+
"[bold red]Commands/Code execution is enabled - Be cautious[/bold red]"
|
229
|
+
)
|
230
|
+
if not (perms.read or perms.write or perms.execute):
|
231
|
+
self.console.print(
|
232
|
+
"[yellow]Note: You currently have no privileges enabled. If you need to interact with files or the system, enable permissions using /read on, /write on, or /execute on.[/yellow]"
|
233
|
+
)
|
234
|
+
|
235
|
+
session = self._create_prompt_session()
|
236
|
+
self._chat_loop(session)
|
237
|
+
|
238
|
+
def _chat_loop(self, session):
|
239
|
+
self.msg_count = 0
|
240
|
+
timer_started = False
|
241
|
+
while True:
|
242
|
+
if not timer_started:
|
243
|
+
timer_started = True
|
244
|
+
cmd_input = self._handle_input(session)
|
245
|
+
if cmd_input is None:
|
246
|
+
break
|
247
|
+
if not cmd_input:
|
248
|
+
continue
|
249
|
+
if self._handle_exit_conditions(cmd_input):
|
250
|
+
break
|
251
|
+
if self._handle_command_input(cmd_input):
|
252
|
+
continue
|
253
|
+
self.user_input_history.append(cmd_input)
|
254
|
+
self._process_prompt(cmd_input)
|
255
|
+
|
256
|
+
def _handle_command_input(self, cmd_input):
|
257
|
+
if cmd_input.startswith("/"):
|
258
|
+
handle_command(cmd_input, shell_state=self.shell_state)
|
259
|
+
return True
|
260
|
+
if cmd_input.startswith("!"):
|
261
|
+
handle_command(f"! {cmd_input[1:]}", shell_state=self.shell_state)
|
262
|
+
return True
|
263
|
+
return False
|
264
|
+
|
265
|
+
def _process_prompt(self, cmd_input):
|
266
|
+
try:
|
267
|
+
# Clear screen before processing new prompt
|
268
|
+
self.console.clear()
|
269
|
+
import time
|
270
|
+
|
271
|
+
final_event = (
|
272
|
+
self._prompt_handler.agent.last_event
|
273
|
+
if hasattr(self._prompt_handler.agent, "last_event")
|
274
|
+
else None
|
275
|
+
)
|
276
|
+
start_time = time.time()
|
277
|
+
|
278
|
+
model_name, provider_name = self._get_model_info()
|
279
|
+
backend_hostname = self._get_backend_hostname()
|
280
|
+
|
281
|
+
self.console.print(
|
282
|
+
Rule(
|
283
|
+
f"[bold blue]Model: {model_name} ({provider_name}) | Backend: {backend_hostname}[/bold blue]"
|
284
|
+
)
|
285
|
+
)
|
286
|
+
|
287
|
+
self._prompt_handler.run_prompt(cmd_input)
|
288
|
+
end_time = time.time()
|
289
|
+
elapsed = end_time - start_time
|
290
|
+
self.msg_count += 1
|
291
|
+
from janito.formatting_token import print_token_message_summary
|
292
|
+
|
293
|
+
usage = self.performance_collector.get_last_request_usage()
|
294
|
+
print_token_message_summary(
|
295
|
+
self.console, self.msg_count, usage, elapsed=elapsed
|
296
|
+
)
|
297
|
+
if final_event and hasattr(final_event, "metadata"):
|
298
|
+
exit_reason = (
|
299
|
+
final_event.metadata.get("exit_reason")
|
300
|
+
if hasattr(final_event, "metadata")
|
301
|
+
else None
|
302
|
+
)
|
303
|
+
if exit_reason:
|
304
|
+
self.console.print(
|
305
|
+
f"[bold yellow]Exit reason: {exit_reason}[/bold yellow]"
|
306
|
+
)
|
307
|
+
except Exception as exc:
|
308
|
+
self.console.print(f"[red]Exception in agent: {exc}[/red]")
|
309
|
+
import traceback
|
310
|
+
|
311
|
+
self.console.print(traceback.format_exc())
|
312
|
+
|
313
|
+
def _extract_args(self, args):
|
314
|
+
"""Extract profile and role arguments from args."""
|
315
|
+
profile = getattr(args, "profile", None) if args is not None else None
|
316
|
+
role_arg = None
|
317
|
+
python_profile = (
|
318
|
+
getattr(args, "developer", False) if args is not None else False
|
319
|
+
)
|
320
|
+
market_profile = getattr(args, "market", False) if args is not None else False
|
321
|
+
return profile, role_arg, python_profile, market_profile
|
322
|
+
|
323
|
+
def _determine_profile(self, profile, python_profile, market_profile):
|
324
|
+
"""Determine the profile based on flags and arguments."""
|
325
|
+
if python_profile and profile is None:
|
326
|
+
return "Developer"
|
327
|
+
if market_profile and profile is None:
|
328
|
+
return "Market Analyst"
|
329
|
+
return profile
|
330
|
+
|
331
|
+
def _should_skip_profile_selection(self, args):
|
332
|
+
"""Check if profile selection should be skipped for getter commands."""
|
333
|
+
from janito.cli.core.getters import GETTER_KEYS
|
334
|
+
|
335
|
+
if args is None:
|
336
|
+
return False
|
337
|
+
|
338
|
+
for key in GETTER_KEYS:
|
339
|
+
if getattr(args, key, False):
|
340
|
+
return True
|
341
|
+
return False
|
342
|
+
|
343
|
+
def _get_model_info(self):
|
344
|
+
"""Get model and provider information."""
|
345
|
+
model_name = (
|
346
|
+
self.agent.get_model_name()
|
347
|
+
if hasattr(self.agent, "get_model_name")
|
348
|
+
else "Unknown"
|
349
|
+
)
|
350
|
+
provider_name = (
|
351
|
+
self.agent.get_provider_name()
|
352
|
+
if hasattr(self.agent, "get_provider_name")
|
353
|
+
else "Unknown"
|
354
|
+
)
|
355
|
+
return model_name, provider_name
|
356
|
+
|
357
|
+
def _get_backend_hostname(self):
|
358
|
+
"""Extract backend hostname from agent configuration."""
|
359
|
+
candidates = self._collect_base_urls()
|
360
|
+
return self._parse_hostname_from_urls(candidates)
|
361
|
+
|
362
|
+
def _collect_base_urls(self):
|
363
|
+
"""Collect all possible base URLs from agent configuration."""
|
364
|
+
candidates = []
|
365
|
+
|
366
|
+
# Collect from driver
|
367
|
+
drv = getattr(self.agent, "driver", None)
|
368
|
+
if drv is not None:
|
369
|
+
cfg = getattr(drv, "config", None)
|
370
|
+
if cfg is not None:
|
371
|
+
b = getattr(cfg, "base_url", None)
|
372
|
+
if b:
|
373
|
+
candidates.append(b)
|
374
|
+
direct_base = getattr(drv, "base_url", None)
|
375
|
+
if direct_base:
|
376
|
+
candidates.append(direct_base)
|
377
|
+
|
378
|
+
# Collect from agent config
|
379
|
+
cfg2 = getattr(self.agent, "config", None)
|
380
|
+
if cfg2 is not None:
|
381
|
+
b2 = getattr(cfg2, "base_url", None)
|
382
|
+
if b2:
|
383
|
+
candidates.append(b2)
|
384
|
+
|
385
|
+
# Collect from agent directly
|
386
|
+
top_base = getattr(self.agent, "base_url", None)
|
387
|
+
if top_base:
|
388
|
+
candidates.append(top_base)
|
389
|
+
|
390
|
+
return candidates
|
391
|
+
|
392
|
+
def _parse_hostname_from_urls(self, candidates):
|
393
|
+
"""Parse hostname from a list of URL candidates."""
|
394
|
+
from urllib.parse import urlparse
|
395
|
+
|
396
|
+
for candidate in candidates:
|
397
|
+
try:
|
398
|
+
if not candidate:
|
399
|
+
continue
|
400
|
+
parsed = urlparse(str(candidate))
|
401
|
+
host = parsed.netloc or parsed.path
|
402
|
+
if host:
|
403
|
+
return host
|
404
|
+
except Exception:
|
405
|
+
return str(candidate)
|
406
|
+
|
407
|
+
return "Unknown"
|
408
|
+
|
409
|
+
def _create_prompt_session(self):
|
410
|
+
return PromptSession(
|
411
|
+
style=chat_shell_style,
|
412
|
+
completer=ShellCommandCompleter(),
|
413
|
+
history=self.mem_history,
|
414
|
+
editing_mode=EditingMode.EMACS,
|
415
|
+
key_bindings=self.key_bindings,
|
416
|
+
bottom_toolbar=lambda: get_toolbar_func(
|
417
|
+
self.performance_collector, 0, self.shell_state
|
418
|
+
)(),
|
419
|
+
multiline=self.multi_line_mode,
|
420
|
+
)
|
421
|
+
|
422
|
+
def _handle_input(self, session):
|
423
|
+
injected = getattr(self.shell_state, "injected_input", None)
|
424
|
+
if injected is not None:
|
425
|
+
cmd_input = injected
|
426
|
+
self.shell_state.injected_input = None
|
427
|
+
else:
|
428
|
+
try:
|
429
|
+
cmd_input = session.prompt(HTML("<inputline>💬 </inputline>"))
|
430
|
+
except KeyboardInterrupt:
|
431
|
+
# Ask for confirmation on Ctrl+C
|
432
|
+
from prompt_toolkit import prompt
|
433
|
+
|
434
|
+
try:
|
435
|
+
confirm = prompt(
|
436
|
+
"Are you sure you want to exit? (y/n): ",
|
437
|
+
style=self._create_prompt_session().style,
|
438
|
+
)
|
439
|
+
if confirm.lower() == "y":
|
440
|
+
self._handle_exit()
|
441
|
+
return None
|
442
|
+
else:
|
443
|
+
return "" # Return empty string to continue
|
444
|
+
except (KeyboardInterrupt, EOFError):
|
445
|
+
# Handle second Ctrl+C or Ctrl+D as immediate exit
|
446
|
+
self._handle_exit()
|
447
|
+
return None
|
448
|
+
except EOFError:
|
449
|
+
self._handle_exit()
|
450
|
+
return None
|
451
|
+
sanitized = cmd_input.strip()
|
452
|
+
try:
|
453
|
+
sanitized.encode("utf-8")
|
454
|
+
except UnicodeEncodeError:
|
455
|
+
sanitized = sanitized.encode("utf-8", errors="replace").decode("utf-8")
|
456
|
+
self.console.print(
|
457
|
+
"[yellow]Warning: Some characters in your input were not valid UTF-8 and have been replaced.[/yellow]"
|
458
|
+
)
|
459
|
+
return sanitized
|
460
|
+
|
461
|
+
def _handle_exit(self):
|
462
|
+
session_duration = time.time() - self.session_start_time
|
463
|
+
|
464
|
+
# Get total token usage from performance collector
|
465
|
+
from janito.perf_singleton import performance_collector
|
466
|
+
|
467
|
+
total_tokens = performance_collector.get_token_usage().get("total_tokens", 0)
|
468
|
+
|
469
|
+
# Format session duration
|
470
|
+
if session_duration < 60:
|
471
|
+
duration_str = f"{session_duration:.1f}s"
|
472
|
+
elif session_duration < 3600:
|
473
|
+
duration_str = f"{session_duration/60:.1f}m"
|
474
|
+
else:
|
475
|
+
duration_str = f"{session_duration/3600:.1f}h"
|
476
|
+
|
477
|
+
# Format tokens in k/m/t as appropriate
|
478
|
+
if total_tokens >= 1_000_000_000:
|
479
|
+
token_str = f"{total_tokens/1_000_000_000:.1f}t"
|
480
|
+
elif total_tokens >= 1_000_000:
|
481
|
+
token_str = f"{total_tokens/1_000_000:.1f}m"
|
482
|
+
elif total_tokens >= 1_000:
|
483
|
+
token_str = f"{total_tokens/1_000:.1f}k"
|
484
|
+
else:
|
485
|
+
token_str = f"{total_tokens}"
|
486
|
+
|
487
|
+
self.console.print(f"[bold yellow]Session completed![/bold yellow]")
|
488
|
+
self.console.print(
|
489
|
+
f"[dim]Session time: {duration_str} | Total tokens: {token_str}[/dim]"
|
490
|
+
)
|
491
|
+
self.console.print("[bold yellow]Goodbye![/bold yellow]")
|
492
|
+
|
493
|
+
if hasattr(self, "agent") and hasattr(self.agent, "join_driver"):
|
494
|
+
if (
|
495
|
+
hasattr(self.agent, "input_queue")
|
496
|
+
and self.agent.input_queue is not None
|
497
|
+
):
|
498
|
+
self.agent.input_queue.put(None)
|
499
|
+
self.agent.join_driver()
|
500
|
+
|
501
|
+
def _handle_exit_conditions(self, cmd_input):
|
502
|
+
if cmd_input.lower() in ("/exit", ":q", ":quit"):
|
503
|
+
self._handle_exit()
|
504
|
+
return True
|
505
|
+
return False
|