janito 3.9.0__py3-none-any.whl → 3.11.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/agent_events.py ADDED
@@ -0,0 +1,75 @@
1
+ import attr
2
+ from typing import Any, ClassVar, Optional
3
+ from janito.event_bus.event import Event
4
+
5
+
6
+ @attr.s(auto_attribs=True, kw_only=True)
7
+ class AgentEvent(Event):
8
+ """
9
+ Base class for events related to an agent.
10
+ Includes agent name for identification.
11
+ """
12
+
13
+ category: ClassVar[str] = "agent"
14
+ agent_name: Optional[str] = None
15
+
16
+
17
+ @attr.s(auto_attribs=True, kw_only=True)
18
+ class AgentInitialized(AgentEvent):
19
+ """Emitted when an agent is initialized."""
20
+ pass
21
+
22
+
23
+ @attr.s(auto_attribs=True, kw_only=True)
24
+ class AgentChatStarted(AgentEvent):
25
+ """Emitted when an agent starts a chat session."""
26
+ prompt: Optional[str] = None
27
+ messages: Optional[list] = None
28
+ role: Optional[str] = None
29
+
30
+
31
+ @attr.s(auto_attribs=True, kw_only=True)
32
+ class AgentChatFinished(AgentEvent):
33
+ """Emitted when an agent completes a chat session."""
34
+ result: Any = None
35
+ loop_count: int = 0
36
+
37
+
38
+ @attr.s(auto_attribs=True, kw_only=True)
39
+ class AgentProcessingResponse(AgentEvent):
40
+ """Emitted when an agent is processing a response."""
41
+ response: Any = None
42
+
43
+
44
+ @attr.s(auto_attribs=True, kw_only=True)
45
+ class AgentToolCallStarted(AgentEvent):
46
+ """Emitted when an agent starts processing a tool call."""
47
+ tool_call_id: Optional[str] = None
48
+ name: Optional[str] = None
49
+ arguments: Any = None
50
+
51
+
52
+ @attr.s(auto_attribs=True, kw_only=True)
53
+ class AgentToolCallFinished(AgentEvent):
54
+ """Emitted when an agent completes processing a tool call."""
55
+ tool_call_id: Optional[str] = None
56
+ name: Optional[str] = None
57
+ result: Any = None
58
+
59
+
60
+ @attr.s(auto_attribs=True, kw_only=True)
61
+ class AgentWaitingForResponse(AgentEvent):
62
+ """Emitted when an agent is waiting for a response from the LLM API."""
63
+ pass
64
+
65
+
66
+ @attr.s(auto_attribs=True, kw_only=True)
67
+ class AgentReceivedResponse(AgentEvent):
68
+ """Emitted when an agent receives a response from the LLM API."""
69
+ response: Any = None
70
+
71
+
72
+ @attr.s(auto_attribs=True, kw_only=True)
73
+ class AgentShutdown(AgentEvent):
74
+ """Emitted when an agent is shutting down."""
75
+ pass
@@ -31,6 +31,7 @@ class ChatShellState:
31
31
  self.mem_history = mem_history
32
32
  self.conversation_history = conversation_history
33
33
  self.paste_mode = False
34
+ self.interactive_mode = True # Default to interactive mode
34
35
 
35
36
  self._pid = None
36
37
  self._stdout_path = None
@@ -11,6 +11,7 @@ from .session import HistoryShellHandler
11
11
  from .tools import ToolsShellHandler
12
12
  from .help import HelpShellHandler
13
13
  from .security_command import SecurityCommand
14
+ from .interactive import InteractiveShellHandler
14
15
  from janito.cli.console import shared_console
15
16
 
16
17
  COMMAND_HANDLERS = {
@@ -45,6 +46,7 @@ COMMAND_HANDLERS = {
45
46
  "/help": HelpShellHandler,
46
47
  "/security": SecurityCommand,
47
48
  "/provider": ProviderCmdHandler,
49
+ "/interactive": InteractiveShellHandler,
48
50
  }
49
51
 
50
52
 
@@ -0,0 +1,33 @@
1
+ from janito.cli.chat_mode.shell.commands.base import ShellCmdHandler
2
+ from janito.cli.console import shared_console
3
+
4
+
5
+ class InteractiveShellHandler(ShellCmdHandler):
6
+ help_text = "Toggle interactive mode on/off"
7
+
8
+ def run(self):
9
+ args = self.after_cmd_line.strip().lower()
10
+
11
+ if args not in ["on", "off"]:
12
+ shared_console.print(
13
+ "[bold red]Usage: /interactive on|off[/bold red]"
14
+ )
15
+ return
16
+
17
+ # Get current interactive state from shell_state if available
18
+ current_state = getattr(self.shell_state, 'interactive_mode', True)
19
+
20
+ if args == "on":
21
+ if current_state:
22
+ shared_console.print("[yellow]Interactive mode is already enabled.[/yellow]")
23
+ else:
24
+ if hasattr(self.shell_state, 'interactive_mode'):
25
+ self.shell_state.interactive_mode = True
26
+ shared_console.print("[green]Interactive mode enabled.[/green]")
27
+ elif args == "off":
28
+ if not current_state:
29
+ shared_console.print("[yellow]Interactive mode is already disabled.[/yellow]")
30
+ else:
31
+ if hasattr(self.shell_state, 'interactive_mode'):
32
+ self.shell_state.interactive_mode = False
33
+ shared_console.print("[green]Interactive mode disabled.[/green]")
@@ -51,6 +51,20 @@ def assemble_bindings_line(width, permissions=None):
51
51
  )
52
52
 
53
53
 
54
+ def assemble_platform_line():
55
+ import platform
56
+ import sys
57
+ from janito.platform_discovery import PlatformDiscovery
58
+
59
+ discovery = PlatformDiscovery()
60
+ system_info = f"{platform.system()} {platform.release()}"
61
+ arch_info = platform.machine()
62
+ shell_info = discovery.detect_shell()
63
+ python_version = f"Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
64
+
65
+ return f" Platform: {system_info} ({arch_info}) | Shell: {shell_info} | {python_version}"
66
+
67
+
54
68
  def _get_status(shell_state):
55
69
  _support = getattr(shell_state, "_support", False)
56
70
  _status = getattr(shell_state, "_status", None)
@@ -96,7 +110,8 @@ def get_toolbar_func(perf: PerformanceCollector, msg_count: int, shell_state):
96
110
  first_line = assemble_first_line(provider_name, model_name, role, agent=agent)
97
111
  permissions = _get_permissions()
98
112
  bindings_line = assemble_bindings_line(width, permissions)
99
- toolbar_text = first_line + "\n" + bindings_line
113
+ platform_line = assemble_platform_line()
114
+ toolbar_text = first_line + "\n" + platform_line + "\n" + bindings_line
100
115
 
101
116
  return HTML(toolbar_text)
102
117
 
janito/cli/core/runner.py CHANGED
@@ -38,6 +38,16 @@ def _choose_provider(args):
38
38
  return provider
39
39
 
40
40
 
41
+ def _select_coder_model_for_provider(provider: str) -> str:
42
+ """Auto-select the best coder model for the given provider when --developer mode is used."""
43
+ coder_models = {
44
+ "alibaba": "qwen3-coder-plus",
45
+ "cerebras": "qwen-3-coder-480b",
46
+ }
47
+
48
+ return coder_models.get(provider)
49
+
50
+
41
51
  def _populate_driver_config_data(args, modifiers, provider, model):
42
52
  from janito.provider_config import get_effective_setting
43
53
 
@@ -80,6 +90,17 @@ def prepare_llm_driver_config(args, modifiers):
80
90
  if not model:
81
91
  model = get_effective_model(provider)
82
92
 
93
+ # Auto-select coder model when --developer mode is used and no model is specified
94
+ if not model and getattr(args, "developer", False):
95
+ model = _select_coder_model_for_provider(provider)
96
+ if model and getattr(args, "verbose", False):
97
+ print_verbose_info(
98
+ "Auto-selected coder model",
99
+ f"{model} for provider {provider} (developer mode)",
100
+ style="magenta",
101
+ align_content=True,
102
+ )
103
+
83
104
  # Validate that the chosen model is supported by the selected provider
84
105
  if model:
85
106
  from janito.provider_registry import ProviderRegistry
@@ -149,6 +170,15 @@ def handle_runner(
149
170
 
150
171
  load_disabled_tools_from_config()
151
172
 
173
+ # Disable bash tools when running in PowerShell
174
+ from janito.platform_discovery import PlatformDiscovery
175
+
176
+ pd = PlatformDiscovery()
177
+ if pd.detect_shell().startswith("PowerShell"):
178
+ from janito.tools.disabled_tools import DisabledToolsState
179
+
180
+ DisabledToolsState.disable_tool("run_bash_command")
181
+
152
182
  unrestricted = getattr(args, "unrestricted", False)
153
183
  adapter = janito.tools.get_local_tools_adapter(
154
184
  workdir=getattr(args, "workdir", None)
@@ -230,4 +260,7 @@ def handle_runner(
230
260
 
231
261
 
232
262
  def get_prompt_mode(args):
263
+ # If interactive flag is set, force chat mode regardless of user_prompt
264
+ if getattr(args, "interactive", False):
265
+ return "chat_mode"
233
266
  return "single_shot" if getattr(args, "user_prompt", None) else "chat_mode"
janito/cli/main_cli.py CHANGED
@@ -204,6 +204,13 @@ definition = [
204
204
  "help": "Enable emoji usage in responses to make output more engaging and expressive",
205
205
  },
206
206
  ),
207
+ (
208
+ ["-i", "--interactive"],
209
+ {
210
+ "action": "store_true",
211
+ "help": "Signal that this is an interactive chat session",
212
+ },
213
+ ),
207
214
  (["user_prompt"], {"nargs": argparse.REMAINDER, "help": "Prompt to submit"}),
208
215
  (
209
216
  ["-e", "--event-log"],
@@ -256,6 +263,7 @@ MODIFIER_KEYS = [
256
263
  "read",
257
264
  "write",
258
265
  "emoji",
266
+ "interactive",
259
267
  ]
260
268
  SETTER_KEYS = ["set", "set_provider", "set_api_key", "unset"]
261
269
  GETTER_KEYS = [
@@ -420,6 +428,7 @@ class JanitoCLI:
420
428
 
421
429
  # If running in single shot mode and --profile is not provided, default to 'developer' profile
422
430
  # Skip profile selection for list commands that don't need it
431
+ # Also skip if interactive mode is enabled (forces chat mode)
423
432
  if get_prompt_mode(self.args) == "single_shot" and not getattr(
424
433
  self.args, "profile", None
425
434
  ):
janito/cli/prompt_core.py CHANGED
@@ -3,6 +3,7 @@ Core PromptHandler: Handles prompt submission and response formatting for janito
3
3
  """
4
4
 
5
5
  import time
6
+ import sys
6
7
  from janito import __version__ as VERSION
7
8
  from janito.performance_collector import PerformanceCollector
8
9
  from rich.status import Status
@@ -78,12 +79,27 @@ class PromptHandler:
78
79
  )
79
80
  return None
80
81
 
82
+ def _clear_current_line(self):
83
+ """
84
+ Clears the current line in the terminal and returns the cursor to column 1.
85
+ """
86
+ # Use raw ANSI escape sequences but write directly to the underlying file
87
+ # to bypass Rich's escaping/interpretation
88
+ if hasattr(self.console, "file") and hasattr(self.console.file, "write"):
89
+ self.console.file.write("\r\033[2K")
90
+ self.console.file.flush()
91
+ else:
92
+ # Fallback to sys.stdout if console.file is not available
93
+ sys.stdout.write("\r\033[2K")
94
+ sys.stdout.flush()
95
+
81
96
  def _handle_tool_call_started(self, inner_event, status):
82
- """Handle ToolCallStarted event - pause the timer when ask_user tool is called."""
83
- if hasattr(inner_event, 'tool_name') and inner_event.tool_name == 'ask_user':
84
- # Pause the status timer by clearing the status
85
- if status:
86
- status.update("")
97
+ """Handle ToolCallStarted event - clear the status before any tool execution."""
98
+ # Always clear the status when any tool starts to avoid cluttering the UI
99
+ if status:
100
+ status.update("")
101
+ # Also clear the current line to ensure clean terminal output
102
+ self._clear_current_line()
87
103
  return None
88
104
 
89
105
  def _handle_tool_call_finished(self, inner_event):
@@ -217,42 +233,60 @@ class PromptHandler:
217
233
  """
218
234
  try:
219
235
  self._print_verbose_debug("Calling agent.chat()...")
220
-
236
+
221
237
  # Show waiting status with elapsed time
222
238
  start_time = time.time()
223
-
239
+
224
240
  # Get provider and model info for status display
225
- provider_name = self.agent.get_provider_name() if hasattr(self.agent, 'get_provider_name') else 'LLM'
226
- model_name = self.agent.get_model_name() if hasattr(self.agent, 'get_model_name') else 'unknown'
227
-
228
- status = Status(f"[bold blue]Waiting for {provider_name} (model: {model_name})...[/bold blue]")
229
-
241
+ provider_name = (
242
+ self.agent.get_provider_name()
243
+ if hasattr(self.agent, "get_provider_name")
244
+ else "LLM"
245
+ )
246
+ model_name = (
247
+ self.agent.get_model_name()
248
+ if hasattr(self.agent, "get_model_name")
249
+ else "unknown"
250
+ )
251
+
252
+ status = Status(
253
+ f"[bold blue]Waiting for {provider_name} (model: {model_name})...[/bold blue]"
254
+ )
230
255
  # Thread coordination event
231
256
  stop_updater = threading.Event()
232
-
257
+
233
258
  def update_status():
234
259
  elapsed = time.time() - start_time
235
- status.update(f"[bold blue]Waiting for {provider_name} (model: {model_name})... ({elapsed:.1f}s)[/bold blue]")
236
-
260
+ status.update(
261
+ f"[bold blue]Waiting for {provider_name} (model: {model_name})... ({elapsed:.1f}s)[/bold blue]"
262
+ )
263
+
237
264
  # Start status display and update timer
238
- with status:
239
- # Update status every second in a separate thread
240
- def status_updater():
241
- while not stop_updater.is_set():
242
- update_status()
243
- stop_updater.wait(1.0) # Wait for 1 second or until stopped
244
-
245
- updater_thread = threading.Thread(target=status_updater, daemon=True)
246
- updater_thread.start()
247
-
248
- try:
249
- final_event = self.agent.chat(prompt=user_prompt)
250
- finally:
251
- # Signal the updater thread to stop
252
- stop_updater.set()
253
- # Wait a bit for the thread to clean up
254
- updater_thread.join(timeout=0.1)
255
-
265
+ status.start()
266
+
267
+ # Update status every second in a separate thread
268
+ def status_updater():
269
+ while not stop_updater.is_set():
270
+ update_status()
271
+ stop_updater.wait(1.0) # Wait for 1 second or until stopped
272
+
273
+ updater_thread = threading.Thread(target=status_updater, daemon=True)
274
+ updater_thread.start()
275
+
276
+ try:
277
+ # Stop status before calling agent.chat() to prevent interference with tools
278
+ status.stop()
279
+ # Clear the current line after status is stopped
280
+ self._clear_current_line()
281
+
282
+ final_event = self.agent.chat(prompt=user_prompt)
283
+ finally:
284
+ # Signal the updater thread to stop
285
+ stop_updater.set()
286
+ # Wait a bit for the thread to clean up
287
+ updater_thread.join(timeout=0.1)
288
+ # Clear the current line after status is suspended/closed
289
+ self._clear_current_line()
256
290
  if hasattr(self.agent, "set_latest_event"):
257
291
  self.agent.set_latest_event(final_event)
258
292
  self.agent.last_event = final_event