janito 3.1.0__py3-none-any.whl → 3.2.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 CHANGED
@@ -106,6 +106,9 @@ janito --set model=kimi-k1-8k
106
106
 
107
107
  ## Advanced Features
108
108
 
109
+ ### 🚀 New in v3.1.0: Enter Key Cancellation
110
+ **Chat Mode Enhancement**: Press **Enter** at any time to instantly cancel long-running requests in interactive chat mode. No more waiting for stuck requests!
111
+
109
112
  ### Tool Usage
110
113
 
111
114
  Janito includes powerful built-in tools for:
@@ -35,30 +35,30 @@ class KeyBindingsFactory:
35
35
  buf.text = "Do It"
36
36
  buf.validate_and_handle()
37
37
 
38
- @bindings.add("enter", eager=True)
38
+ @bindings.add("c-c")
39
39
  def _(event):
40
- """Handle Enter key to interrupt current request."""
41
- import threading
42
-
43
- # Get the current session context
44
- from prompt_toolkit.application import get_app
45
- app = get_app()
46
-
40
+ """Handle Ctrl+C to interrupt current request or exit chat."""
47
41
  # Use global cancellation manager for robust cancellation
48
42
  from janito.llm.cancellation_manager import get_cancellation_manager
43
+
49
44
  cancel_manager = get_cancellation_manager()
50
-
45
+
51
46
  cancelled = cancel_manager.cancel_current_request()
52
47
  if cancelled:
53
48
  # Provide user feedback
54
49
  from rich.console import Console
50
+
55
51
  console = Console()
56
- console.print("[red]Request cancelled by Enter key[/red]")
57
-
58
- # Prevent the Enter key from being processed as input
52
+ console.print("[red]Request cancelled by Ctrl+C[/red]")
53
+
54
+ # Prevent the Ctrl+C from being processed as input
59
55
  event.app.output.flush()
60
56
  return
61
-
62
- # If no active request to cancel, let normal Enter behavior proceed
57
+ else:
58
+ # No active request to cancel, exit the chat
59
+ from rich.console import Console
60
+ console = Console()
61
+ console.print("[yellow]Goodbye![/yellow]")
62
+ event.app.exit()
63
63
 
64
64
  return bindings
@@ -22,6 +22,7 @@ import time
22
22
 
23
23
  # Shared prompt/agent factory
24
24
  from janito.cli.prompt_setup import setup_agent_and_prompt_handler
25
+ from janito.llm.cancellation_manager import get_cancellation_manager
25
26
 
26
27
  import time
27
28
 
@@ -113,6 +114,10 @@ class ChatSession:
113
114
 
114
115
  # Check if multi-line mode should be enabled by default
115
116
  self.multi_line_mode = getattr(args, "multi", False) if args else False
117
+
118
+ # Default to single-line mode (Enter submits) unless explicitly enabled
119
+ if not self.multi_line_mode:
120
+ self.multi_line_mode = False
116
121
 
117
122
  def _select_profile_and_role(self, args, role):
118
123
  profile, role_arg, python_profile, market_profile = self._extract_args(args)
@@ -289,7 +294,7 @@ class ChatSession:
289
294
  # Ensure cancellation manager is cleared
290
295
  cancel_manager = get_cancellation_manager()
291
296
  cancel_manager.clear_current_request()
292
-
297
+
293
298
  end_time = time.time()
294
299
  elapsed = end_time - start_time
295
300
  self.msg_count += 1
@@ -59,39 +59,53 @@ Examples:
59
59
  command = args[0].lower()
60
60
  whitelist_manager = get_url_whitelist_manager()
61
61
 
62
- if command == "list":
63
- sites = whitelist_manager.get_allowed_sites()
64
- if sites:
65
- print("Allowed sites:")
66
- for site in sites:
67
- print(f" • {site}")
68
- else:
69
- print("No sites are whitelisted (all sites are allowed)")
70
-
71
- elif command == "add":
72
- if len(args) < 2:
73
- print("Error: Please specify a site to add")
74
- return
75
- site = args[1]
76
- if whitelist_manager.add_allowed_site(site):
77
- print(f"✅ Added '{site}' to allowed sites")
78
- else:
79
- print(f"ℹ️ '{site}' is already in allowed sites")
80
-
81
- elif command == "remove":
82
- if len(args) < 2:
83
- print("Error: Please specify a site to remove")
84
- return
85
- site = args[1]
86
- if whitelist_manager.remove_allowed_site(site):
87
- print(f"✅ Removed '{site}' from allowed sites")
88
- else:
89
- print(f"ℹ️ '{site}' was not in allowed sites")
90
-
91
- elif command == "clear":
92
- whitelist_manager.clear_whitelist()
93
- print("✅ Cleared all allowed sites (all sites are now allowed)")
94
-
62
+ handlers = {
63
+ "list": self._handle_list,
64
+ "add": self._handle_add,
65
+ "remove": self._handle_remove,
66
+ "clear": self._handle_clear,
67
+ }
68
+
69
+ handler = handlers.get(command)
70
+ if handler:
71
+ handler(args, whitelist_manager)
95
72
  else:
96
73
  print(f"Error: Unknown command '{command}'")
97
74
  print(self.get_usage())
75
+
76
+ def _handle_list(self, args, whitelist_manager):
77
+ """Handle list command."""
78
+ sites = whitelist_manager.get_allowed_sites()
79
+ if sites:
80
+ print("Allowed sites:")
81
+ for site in sites:
82
+ print(f" • {site}")
83
+ else:
84
+ print("No sites are whitelisted (all sites are allowed)")
85
+
86
+ def _handle_add(self, args, whitelist_manager):
87
+ """Handle add command."""
88
+ if len(args) < 2:
89
+ print("Error: Please specify a site to add")
90
+ return
91
+ site = args[1]
92
+ if whitelist_manager.add_allowed_site(site):
93
+ print(f"✅ Added '{site}' to allowed sites")
94
+ else:
95
+ print(f"ℹ️ '{site}' is already in allowed sites")
96
+
97
+ def _handle_remove(self, args, whitelist_manager):
98
+ """Handle remove command."""
99
+ if len(args) < 2:
100
+ print("Error: Please specify a site to remove")
101
+ return
102
+ site = args[1]
103
+ if whitelist_manager.remove_allowed_site(site):
104
+ print(f"✅ Removed '{site}' from allowed sites")
105
+ else:
106
+ print(f"ℹ️ '{site}' was not in allowed sites")
107
+
108
+ def _handle_clear(self, args, whitelist_manager):
109
+ """Handle clear command."""
110
+ whitelist_manager.clear_whitelist()
111
+ print("✅ Cleared all allowed sites (all sites are now allowed)")
@@ -21,34 +21,50 @@ def guess_provider_from_model(model_name: str) -> str:
21
21
  model_name = model_name.lower()
22
22
 
23
23
  # Check each provider's models
24
+ return _find_provider_for_model(model_name)
25
+
26
+
27
+ def _find_provider_for_model(model_name: str) -> str:
28
+ """Find provider for given model name."""
24
29
  for provider_name in LLMProviderRegistry.list_providers():
25
30
  provider_class = LLMProviderRegistry.get(provider_name)
26
31
  if not provider_class:
27
32
  continue
28
33
 
29
- # Get model specs for this provider
30
- try:
31
- if hasattr(provider_class, "MODEL_SPECS"):
32
- model_specs = provider_class.MODEL_SPECS
33
- for spec_model_name in model_specs.keys():
34
- if spec_model_name.lower() == model_name:
35
- return provider_name
36
-
37
- # Handle special cases like moonshot
38
- if provider_name == "moonshot":
39
- try:
40
- from janito.providers.moonshot.model_info import (
41
- MOONSHOT_MODEL_SPECS,
42
- )
43
-
44
- for spec_model_name in MOONSHOT_MODEL_SPECS.keys():
45
- if spec_model_name.lower() == model_name:
46
- return "moonshot"
47
- except ImportError:
48
- pass
49
-
50
- except Exception:
51
- # Skip providers that have issues accessing model specs
52
- continue
34
+ if _check_provider_models(provider_name, provider_class, model_name):
35
+ return provider_name
53
36
 
54
37
  return None
38
+
39
+
40
+ def _check_provider_models(provider_name: str, provider_class, model_name: str) -> bool:
41
+ """Check if provider has matching model."""
42
+ try:
43
+ if hasattr(provider_class, "MODEL_SPECS"):
44
+ model_specs = provider_class.MODEL_SPECS
45
+ for spec_model_name in model_specs.keys():
46
+ if spec_model_name.lower() == model_name:
47
+ return True
48
+
49
+ # Handle special cases like moonshot
50
+ if provider_name == "moonshot":
51
+ return _check_moonshot_models(model_name)
52
+
53
+ except Exception:
54
+ # Skip providers that have issues accessing model specs
55
+ pass
56
+
57
+ return False
58
+
59
+
60
+ def _check_moonshot_models(model_name: str) -> bool:
61
+ """Check moonshot models specifically."""
62
+ try:
63
+ from janito.providers.moonshot.model_info import MOONSHOT_MODEL_SPECS
64
+
65
+ for spec_model_name in MOONSHOT_MODEL_SPECS.keys():
66
+ if spec_model_name.lower() == model_name:
67
+ return True
68
+ except ImportError:
69
+ pass
70
+ return False
janito/cli/prompt_core.py CHANGED
@@ -207,12 +207,13 @@ class PromptHandler:
207
207
  """
208
208
  try:
209
209
  self._print_verbose_debug("Calling agent.chat()...")
210
-
210
+
211
211
  # Use global cancellation manager
212
212
  from janito.llm.cancellation_manager import get_cancellation_manager
213
+
213
214
  cancel_manager = get_cancellation_manager()
214
215
  driver_cancel_event = cancel_manager.start_new_request()
215
-
216
+
216
217
  try:
217
218
  final_event = self.agent.chat(prompt=user_prompt)
218
219
  if hasattr(self.agent, "set_latest_event"):
@@ -225,7 +226,7 @@ class PromptHandler:
225
226
  global_event_bus.publish(final_event)
226
227
  finally:
227
228
  cancel_manager.clear_current_request()
228
-
229
+
229
230
  except KeyboardInterrupt:
230
231
  # Capture user interrupt / cancellation
231
232
  self.console.print("[red]Interrupted by the user.[/red]")
janito/i18n/it.py CHANGED
@@ -1,46 +1,46 @@
1
- # pragma: allowlist secret
2
- translations = {
3
- "36107ed78ab25f6fb12ad8ce13018cd1ce6735d1": "Avvio del server web...",
4
- "70a0d194687568a47aa617fd85036ace1e69a982": "Vuoi davvero uscire? (s/n): ",
5
- "5c9ebcbbd7632ecb328bd52958b17158afaa32c6": "F12 = Azione Rapida (segue l'azione raccomandata)",
6
- "fe21121e2934234b68d19b2757532117d440c1e3": "Chiave API non trovata. Si prega di configurare 'api_key' nel file di configurazione.",
7
- "c9e3759b1756eba35b381ce2b72cd659e132b01f": "Ciao, {name}!",
8
- "ca1fee2f55baabdc2e4b0e9529c89ee024e62079": "Nessun prompt fornito nei messaggi",
9
- "f7449d23d0c500ae2a0b31e04f92b47a4d8ae845": "max_tokens deve essere un intero, ricevuto: {resolved_max_tokens!r}",
10
- "70a9ed8edb6da12e208431a31aa16ba54419b26f": "Risposta non valida/malformed da OpenAI (tentativo {attempt}/{max_retries}). Riprovo tra {wait_time} secondi...",
11
- "a873085e3b06184fb5d27e842f97b06b6190976d": "Numero massimo di tentativi per risposta non valida raggiunto. Generazione errore.",
12
- "66a34568bbe846bb1bde3619eb4d6dfa10211104": "L'API non supporta l'uso degli strumenti.",
13
- "09b81476b75586da4116b83f8be70d77b174cec3": "Limite di richieste API OpenAI (429) (tentativo {attempt}/{max_retries}): {e}. Riprovo tra {wait_time} secondi...",
14
- "5717a35dd2a1533fb7e15edc8c9329cb69f3410b": "Errore server API OpenAI (tentativo {attempt}/{max_retries}): {e}. Riprovo tra {wait_time} secondi...",
15
- "02e760ba15ed863176c1290ac8a9b923963103cd": "Errore client API OpenAI {status_code}: {e}. Nessun nuovo tentativo.",
16
- "2e52b0bbc8f16226b70e3e20f95c9245d2bcdb47": "Errore API OpenAI (tentativo {attempt}/{max_retries}): {e}. Riprovo tra {wait_time} secondi...",
17
- "012cc970e039fdd79c452fc676202c814ffc76ae": "Numero massimo di tentativi per errore API OpenAI raggiunto. Generazione errore.",
18
- "d0438e45667d31e0022b2497b5901cd4300f084b": "QueuedMessageHandler.handle_message si aspetta un dizionario con 'type' e 'message', ricevuto {msg_type}: {msg!r}",
19
- "9d3460187ffa19c7c8a4020157072b1087e1bd2f": "[QueuedMessageHandler] {msg_type}: {msg}",
20
- "3813833343430e8afa8fce33385c5e39fb24dd60": "[QueuedMessageHandler] {msg_type}: {message}",
21
- "0be9a22226e16a40797010d23a0f581542dca40e": "[ToolExecutor] {tool_name} chiamato con argomenti: {args}",
22
- "42c68edcb25442f518b1af77c6a2ddc07461aae0": "[ToolExecutor] Motivo chiamata: {tool_call_reason}",
23
- "002ff598115d84595ffeee6219cb5c03d3a1d4a6": "Domanda",
24
- "35747d13dcd91e8e8790c7f767d5ed764f541b9e": "procedi",
25
- "33dde3a1afbc418768a69fa53168d9b0638fe1aa": "avanti",
26
- "eee0bbba4ff92adbeb038a77df0466d660f15716": "continua",
27
- "edee9402d198b04ac77dcf5dc9cc3dac44573782": "prossimo",
28
- "8fdb7e2fa84f4faf0d9b92f466df424ec47a165b": "ok",
29
- "5f8f924671cda79b5205a6bf1b776f347c4a7a07": "Opzioni di configurazione disponibili:\n",
30
- "cef780a309cd234750764d42697882c24168ddab": "{key:15} {desc} (predefinito: {default})",
31
- "68c2cc7f0ceaa3e499ecb4db331feb4debbbcc23": "Modello",
32
- "c3f104d1365744b538bfde9f4adb6a6df4b80355": "Funzione",
33
- "99a0efc6cfd85d8ff2732a6718140f822cb90472": "Stile",
34
- "f1702b4686278becffc88baabe6f4b7a8355532c": "Messaggi",
35
- "c38c6c1f3a2743f8626703abb302e403d20ff81c": "Token",
36
- "a817d7eb8e0f1dab755ab5203a082e5c3c094fce": "Prompt",
37
- "2ff255684a2277f806fcebf3fe338ed27857f350": "Completamento",
38
- "b25928c69902557b0ef0a628490a3a1768d7b82f": "Totale",
39
- "76e63d65c883ed50df40ac3aeef0c2d6a1c4ad60": "Azione Rapida",
40
- "5397e0583f14f6c88de06b1ef28f460a1fb5b0ae": "Sì",
41
- "816c52fd2bdd94a63cd0944823a6c0aa9384c103": "No",
42
- "c47ae15370cfe1ed2781eedc1dc2547d12d9e972": "Aiuto",
43
- "cc3dbd47e1cf9003a55d3366b3adbcd72275e525": "Nuovo Task",
44
- "efa5a8b84e1afe65c81ecfce28c398c48f19ddc2": "Benvenuto su Janito{version_str}! Modalità chat attiva. Digita /exit per uscire.",
45
- "b314d6e1460f86e0f21abc5aceb7935a2a0667e8": "Benvenuto su Janito{version_str} in modalità [white on magenta]VANILLA[/white on magenta]! Strumenti, prompt di sistema e temperatura sono disattivati, a meno che non siano sovrascritti.",
46
- }
1
+ # pragma: allowlist secret
2
+ translations = {
3
+ "36107ed78ab25f6fb12ad8ce13018cd1ce6735d1": "Avvio del server web...",
4
+ "70a0d194687568a47aa617fd85036ace1e69a982": "Vuoi davvero uscire? (s/n): ",
5
+ "5c9ebcbbd7632ecb328bd52958b17158afaa32c6": "F12 = Azione Rapida (segue l'azione raccomandata)",
6
+ "fe21121e2934234b68d19b2757532117d440c1e3": "Chiave API non trovata. Si prega di configurare 'api_key' nel file di configurazione.",
7
+ "c9e3759b1756eba35b381ce2b72cd659e132b01f": "Ciao, {name}!",
8
+ "ca1fee2f55baabdc2e4b0e9529c89ee024e62079": "Nessun prompt fornito nei messaggi",
9
+ "f7449d23d0c500ae2a0b31e04f92b47a4d8ae845": "max_tokens deve essere un intero, ricevuto: {resolved_max_tokens!r}",
10
+ "70a9ed8edb6da12e208431a31aa16ba54419b26f": "Risposta non valida/malformed da OpenAI (tentativo {attempt}/{max_retries}). Riprovo tra {wait_time} secondi...",
11
+ "a873085e3b06184fb5d27e842f97b06b6190976d": "Numero massimo di tentativi per risposta non valida raggiunto. Generazione errore.",
12
+ "66a34568bbe846bb1bde3619eb4d6dfa10211104": "L'API non supporta l'uso degli strumenti.",
13
+ "09b81476b75586da4116b83f8be70d77b174cec3": "Limit di richieste API OpenAI (429) (tentativo {attempt}/{max_retries}): {e}. Riprovo tra {wait_time} secondi...",
14
+ "5717a35dd2a1533fb7e15edc8c9329cb69f3410b": "Errore server API OpenAI (tentativo {attempt}/{max_retries}): {e}. Riprovo tra {wait_time} secondi...",
15
+ "02e760ba15ed863176c1290ac8a9b923963103cd": "Errore client API OpenAI {status_code}: {e}. Nessun nuovo tentativo.",
16
+ "2e52b0bbc8f16226b70e3e20f95c9245d2bcdb47": "Errore API OpenAI (tentativo {attempt}/{max_retries}): {e}. Riprovo tra {wait_time} secondi...",
17
+ "012cc970e039fdd79c452fc676202c814ffc76ae": "Numero massimo di tentativi per errore API OpenAI raggiunto. Generazione errore.",
18
+ "d0438e45667d31e0022b2497b5901cd4300f084b": "QueuedMessageHandler.handle_message si aspetta un dizionario con 'type' e 'message', ricevuto {msg_type}: {msg!r}",
19
+ "9d3460187ffa19c7c8a4020157072b1087e1bd2f": "[QueuedMessageHandler] {msg_type}: {msg}",
20
+ "3813833343430e8afa8fce33385c5e39fb24dd60": "[QueuedMessageHandler] {msg_type}: {message}",
21
+ "0be9a22226e16a40797010d23a0f581542dca40e": "[ToolExecutor] {tool_name} chiamato con argomenti: {args}",
22
+ "42c68edcb25442f518b1af77c6a2ddc07461aae0": "[ToolExecutor] Motivo chiamata: {tool_call_reason}",
23
+ "002ff598115d84595ffeee6219cb5c03d3a1d4a6": "Domanda",
24
+ "35747d13dcd91e8e8790c7f767d5ed764f541b9e": "procedi",
25
+ "33dde3a1afbc418768a69fa53168d9b0638fe1aa": "avanti",
26
+ "eee0bbba4ff92adbeb038a77df0466d660f15716": "continua",
27
+ "edee9402d198b04ac77dcf5dc9cc3dac44573782": "prossimo",
28
+ "8fdb7e2fa84f4faf0d9b92f466df424ec47a165b": "ok",
29
+ "5f8f924671cda79b5205a6bf1b776f347c4a7a07": "Opzioni di configurazione disponibili:\n",
30
+ "cef780a309cd234750764d42697882c24168ddab": "{key:15} {desc} (predefinito: {default})",
31
+ "68c2cc7f0ceaa3e499ecb4db331feb4debbbcc23": "Modello",
32
+ "c3f104d1365744b538bfde9f4adb6a6df4b80355": "Funzione",
33
+ "99a0efc6cfd85d8ff2732a6718140f822cb90472": "Stile",
34
+ "f1702b4686278becffc88baabe6f4b7a8355532c": "Messaggi",
35
+ "c38c6c1f3a2743f8626703abb302e403d20ff81c": "Token",
36
+ "a817d7eb8e0f1dab755ab5203a082e5c3c094fce": "Prompt",
37
+ "2ff255684a2277f806fcebf3fe338ed27857f350": "Completamento",
38
+ "b25928c69902557b0ef0a628490a3a1768d7b82f": "Totale",
39
+ "76e63d65c883ed50df40ac3aeef0c2d6a1c4ad60": "Azione Rapida",
40
+ "5397e0583f14f6c88de06b1ef28f460a1fb5b0ae": "Sì",
41
+ "816c52fd2bdd94a63cd0944823a6c0aa9384c103": "No",
42
+ "c47ae15370cfe1ed2781eedc1dc2547d12d9e972": "Aiuto",
43
+ "cc3dbd47e1cf9003a55d3366b3adbcd72275e525": "Nuovo Task",
44
+ "efa5a8b84e1afe65c81ecfce28c398c48f19ddc2": "Benvenuto su Janito{version_str}! Modalità chat attiva. Digita /exit per uscire.",
45
+ "b314d6e1460f86e0f21abc5aceb7935a2a0667e8": "Benvenuto su Janito{version_str} in modalità [white on magenta]VANILLA[/white on magenta]! Strumenti, prompt di sistema e temperatura sono disattivati, a meno che non siano sovrascritti.",
46
+ }
janito/llm/agent.py CHANGED
@@ -320,16 +320,19 @@ class LLMAgent:
320
320
 
321
321
  # Use global cancellation manager
322
322
  from janito.llm.cancellation_manager import get_cancellation_manager
323
+
323
324
  cancel_manager = get_cancellation_manager()
324
325
  driver_cancel_event = cancel_manager.start_new_request()
325
-
326
+
326
327
  # Store cancellation event on agent for external access
327
328
  self.cancel_event = driver_cancel_event
328
-
329
+
329
330
  try:
330
331
  while True:
331
332
  self._print_verbose_chat_loop(loop_count)
332
- driver_input = self._prepare_driver_input(config, cancel_event=driver_cancel_event)
333
+ driver_input = self._prepare_driver_input(
334
+ config, cancel_event=driver_cancel_event
335
+ )
333
336
  self.input_queue.put(driver_input)
334
337
  try:
335
338
  result, added_tool_results = self._process_next_response()
@@ -346,8 +349,8 @@ class LLMAgent:
346
349
  finally:
347
350
  cancel_manager.clear_current_request()
348
351
  # Clean up cancellation event
349
- if hasattr(self, 'cancel_event'):
350
- delattr(self, 'cancel_event')
352
+ if hasattr(self, "cancel_event"):
353
+ delattr(self, "cancel_event")
351
354
 
352
355
  def _clear_driver_queues(self):
353
356
  if hasattr(self, "driver") and self.driver:
@@ -9,25 +9,26 @@ from typing import Optional
9
9
 
10
10
  class CancellationManager:
11
11
  """Manages cancellation of LLM requests across the application."""
12
-
12
+
13
13
  def __init__(self):
14
14
  self._current_cancel_event: Optional[threading.Event] = None
15
15
  self._lock = threading.Lock()
16
16
  self._keyboard_cancellation = None
17
-
17
+
18
18
  def start_new_request(self) -> threading.Event:
19
19
  """Start a new request and return its cancellation event."""
20
20
  with self._lock:
21
21
  # Create new cancellation event for this request
22
22
  self._current_cancel_event = threading.Event()
23
-
23
+
24
24
  # Start keyboard monitoring
25
25
  from janito.llm.enter_cancellation import get_enter_cancellation
26
+
26
27
  self._keyboard_cancellation = get_enter_cancellation()
27
28
  self._keyboard_cancellation.start_monitoring(self._current_cancel_event)
28
-
29
+
29
30
  return self._current_cancel_event
30
-
31
+
31
32
  def cancel_current_request(self) -> bool:
32
33
  """Cancel the current request if one is active."""
33
34
  with self._lock:
@@ -35,12 +36,12 @@ class CancellationManager:
35
36
  self._current_cancel_event.set()
36
37
  return True
37
38
  return False
38
-
39
+
39
40
  def get_current_cancel_event(self) -> Optional[threading.Event]:
40
41
  """Get the current cancellation event."""
41
42
  with self._lock:
42
43
  return self._current_cancel_event
43
-
44
+
44
45
  def clear_current_request(self):
45
46
  """Clear the current request cancellation event."""
46
47
  with self._lock:
@@ -59,4 +60,4 @@ def get_cancellation_manager() -> CancellationManager:
59
60
  global _global_manager
60
61
  if _global_manager is None:
61
62
  _global_manager = CancellationManager()
62
- return _global_manager
63
+ return _global_manager
janito/llm/driver.py CHANGED
@@ -257,5 +257,6 @@ class LLMDriver(ABC):
257
257
  """Cancel the current request being processed."""
258
258
  # Use global cancellation manager to cancel the current request
259
259
  from janito.llm.cancellation_manager import get_cancellation_manager
260
+
260
261
  cancel_manager = get_cancellation_manager()
261
262
  cancel_manager.cancel_current_request()
@@ -11,75 +11,89 @@ from typing import Optional
11
11
 
12
12
  class EnterCancellation:
13
13
  """Handles Enter key cancellation of LLM requests."""
14
-
14
+
15
15
  def __init__(self):
16
16
  self._cancel_event: Optional[threading.Event] = None
17
17
  self._listener_thread: Optional[threading.Thread] = None
18
18
  self._listening = False
19
-
19
+
20
20
  def start_monitoring(self, cancel_event: threading.Event):
21
21
  """Start monitoring for Enter key to cancel the request."""
22
22
  if self._listening:
23
23
  return
24
-
24
+
25
25
  self._cancel_event = cancel_event
26
26
  self._listening = True
27
-
27
+
28
28
  self._listener_thread = threading.Thread(
29
29
  target=self._monitor_enter_key, daemon=True
30
30
  )
31
31
  self._listener_thread.start()
32
-
32
+
33
33
  def stop_monitoring(self):
34
34
  """Stop monitoring for keyboard input."""
35
35
  self._listening = False
36
36
  if self._listener_thread and self._listener_thread.is_alive():
37
37
  self._listener_thread.join(timeout=0.1)
38
-
38
+
39
39
  def _monitor_enter_key(self):
40
40
  """Monitor for Enter key press."""
41
41
  try:
42
- import sys
43
- import select
44
- import msvcrt # Windows-specific keyboard input
45
-
46
- # Monitor for Enter key (Windows-specific implementation)
47
- while self._listening and self._cancel_event and not self._cancel_event.is_set():
48
- try:
49
- # Windows-specific: check if key is available
50
- if msvcrt.kbhit():
51
- char = msvcrt.getch()
52
- # Check for Enter key (carriage return or newline)
53
- if char in [b'\r', b'\n']:
54
- if self._cancel_event:
55
- self._cancel_event.set()
56
- break
57
- else:
58
- # Discard any other input
59
- pass
60
- else:
61
- # Small delay to prevent high CPU usage
62
- time.sleep(0.05)
63
-
64
- except (IOError, OSError):
65
- break
66
- except Exception:
67
- # Fallback to select-based approach for non-Windows
68
- try:
69
- if sys.stdin in select.select([sys.stdin], [], [], 0.05)[0]:
70
- char = sys.stdin.read(1)
71
- if char in ['\r', '\n']: # Enter key
72
- if self._cancel_event:
73
- self._cancel_event.set()
74
- break
75
- except:
76
- pass
77
- time.sleep(0.05)
78
-
42
+ self._handle_key_monitoring()
79
43
  except Exception:
80
44
  # Silently handle any errors
81
45
  pass
82
46
 
47
+ def _handle_key_monitoring(self):
48
+ """Handle the actual key monitoring logic."""
49
+ import sys
50
+ import select
51
+
52
+ try:
53
+ import msvcrt # Windows-specific keyboard input
54
+
55
+ self._monitor_windows_keys()
56
+ except ImportError:
57
+ self._monitor_unix_keys()
58
+
59
+ def _monitor_windows_keys(self):
60
+ """Monitor keys on Windows systems."""
61
+ import msvcrt
62
+
63
+ while (
64
+ self._listening and self._cancel_event and not self._cancel_event.is_set()
65
+ ):
66
+ try:
67
+ if msvcrt.kbhit():
68
+ char = msvcrt.getch()
69
+ if char in [b"\r", b"\n"]:
70
+ if self._cancel_event:
71
+ self._cancel_event.set()
72
+ break
73
+ else:
74
+ time.sleep(0.05)
75
+ except (IOError, OSError):
76
+ break
77
+
78
+ def _monitor_unix_keys(self):
79
+ """Monitor keys on Unix-like systems."""
80
+ import sys
81
+ import select
82
+
83
+ while (
84
+ self._listening and self._cancel_event and not self._cancel_event.is_set()
85
+ ):
86
+ try:
87
+ if sys.stdin in select.select([sys.stdin], [], [], 0.05)[0]:
88
+ char = sys.stdin.read(1)
89
+ if char in ["\r", "\n"]:
90
+ if self._cancel_event:
91
+ self._cancel_event.set()
92
+ break
93
+ time.sleep(0.05)
94
+ except:
95
+ time.sleep(0.05)
96
+
83
97
 
84
98
  # Global instance
85
99
  _global_enter_cancellation = None
@@ -90,4 +104,4 @@ def get_enter_cancellation() -> EnterCancellation:
90
104
  global _global_enter_cancellation
91
105
  if _global_enter_cancellation is None:
92
106
  _global_enter_cancellation = EnterCancellation()
93
- return _global_enter_cancellation
107
+ return _global_enter_cancellation