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.
@@ -1,179 +1,170 @@
1
- from rich.console import Console
2
- from rich.markdown import Markdown
3
- from rich.pretty import Pretty
4
- from rich.panel import Panel
5
- from rich.text import Text
6
- from janito.event_bus.handler import EventHandlerBase
7
- import janito.driver_events as driver_events
8
- from janito.report_events import ReportSubtype, ReportAction
9
- from janito.event_bus.bus import event_bus
10
- from janito.llm import message_parts
11
-
12
-
13
- import sys
14
-
15
-
16
- class RichTerminalReporter(EventHandlerBase):
17
- """
18
- Handles UI rendering for janito events using Rich.
19
-
20
- - For ResponseReceived events, iterates over the 'parts' field and displays each part appropriately:
21
- - TextMessagePart: rendered as Markdown (uses 'content' field)
22
- - Other MessageParts: displayed using Pretty or a suitable Rich representation
23
- - For RequestFinished events, output is printed only if raw mode is enabled (using Pretty formatting).
24
- - Report events (info, success, error, etc.) are always printed with appropriate styling.
25
- """
26
-
27
- def __init__(self, raw_mode=False):
28
- from janito.cli.console import shared_console
29
-
30
- self.console = shared_console
31
- self.raw_mode = raw_mode
32
- import janito.report_events as report_events
33
-
34
- import janito.tools.tool_events as tool_events
35
-
36
- super().__init__(driver_events, report_events, tool_events)
37
- self._waiting_printed = False
38
-
39
- def on_RequestStarted(self, event):
40
- # Print waiting message with provider and model name
41
- provider = None
42
- model = None
43
- if hasattr(event, "payload") and isinstance(event.payload, dict):
44
- provider = event.payload.get("provider_name")
45
- model = event.payload.get("model") or event.payload.get("model_name")
46
- if not provider:
47
- provider = getattr(event, "provider_name", None)
48
- if not provider:
49
- provider = getattr(event, "driver_name", None)
50
- if not provider:
51
- provider = "LLM"
52
- if not model:
53
- model = getattr(event, "model", None)
54
- if not model:
55
- model = getattr(event, "model_name", None)
56
- if not model:
57
- model = "?"
58
- self.console.print(
59
- f"[bold cyan]Waiting for {provider} (model: {model})...[/bold cyan]", end=""
60
- )
61
-
62
- def on_ResponseReceived(self, event):
63
- parts = event.parts if hasattr(event, "parts") else None
64
- if not parts:
65
- self.console.print("[No response parts to display]")
66
- self.console.file.flush()
67
- return
68
- for part in parts:
69
- if isinstance(part, message_parts.TextMessagePart):
70
- self.console.print(Markdown(part.content))
71
- self.console.file.flush()
72
-
73
- def delete_current_line(self):
74
- """
75
- Clears the entire current line in the terminal and returns the cursor to column 1.
76
- """
77
- # Use raw ANSI escape sequences but write directly to the underlying file
78
- # to bypass Rich's escaping/interpretation
79
- if hasattr(self.console, 'file') and hasattr(self.console.file, 'write'):
80
- self.console.file.write("\r\033[2K")
81
- self.console.file.flush()
82
- else:
83
- # Fallback to sys.stdout if console.file is not available
84
- import sys
85
- sys.stdout.write("\r\033[2K")
86
- sys.stdout.flush()
87
-
88
- def on_RequestFinished(self, event):
89
- self.delete_current_line()
90
- self._waiting_printed = False
91
- response = getattr(event, "response", None)
92
- error = getattr(event, "error", None)
93
- exception = getattr(event, "exception", None)
94
-
95
- # Print error and exception if present
96
- if error:
97
- self.console.print(f"[bold red]Error:[/] {error}")
98
- self.console.file.flush()
99
- if exception:
100
- self.console.print(f"[red]Exception:[/] {exception}")
101
- self.console.file.flush()
102
-
103
- if response is not None:
104
- if self.raw_mode:
105
- self.console.print(Pretty(response, expand_all=True))
106
- self.console.file.flush()
107
- # Check for 'code' and 'event' fields in the response
108
- code = None
109
- event_field = None
110
- if isinstance(response, dict):
111
- code = response.get("code")
112
- event_field = response.get("event")
113
- if event_field is not None:
114
- self.console.print(f"[bold yellow]Event:[/] {event_field}")
115
- self.console.file.flush()
116
- # No output if not raw_mode or if response is None
117
-
118
- def on_ToolCallError(self, event):
119
- # Optionally handle tool call errors in a user-friendly way
120
- error = getattr(event, "error", None)
121
- tool = getattr(event, "tool_name", None)
122
- if error and tool:
123
- self.console.print(f"[bold red]Tool Error ({tool}):[/] {error}")
124
- self.console.file.flush()
125
-
126
- def on_ReportEvent(self, event):
127
- # Special handling for security-related report events
128
- subtype = getattr(event, "subtype", None)
129
- msg = getattr(event, "message", None)
130
- action = getattr(event, "action", None)
131
- tool = getattr(event, "tool", None)
132
- context = getattr(event, "context", None)
133
- if (
134
- subtype == ReportSubtype.ERROR
135
- and msg
136
- and "[SECURITY] Path access denied" in msg
137
- ):
138
- # Highlight security errors with a distinct style
139
- self.console.print(
140
- Panel(f"{msg}", title="[red]SECURITY VIOLATION[/red]", style="bold red")
141
- )
142
- self.console.file.flush()
143
- return
144
-
145
- msg = event.message if hasattr(event, "message") else None
146
- subtype = event.subtype if hasattr(event, "subtype") else None
147
- if not msg or not subtype:
148
- return
149
- if subtype == ReportSubtype.ACTION_INFO:
150
- # Use orange for all write/modification actions
151
- modification_actions = (
152
- getattr(ReportAction, "UPDATE", None),
153
- getattr(ReportAction, "WRITE", None),
154
- getattr(ReportAction, "DELETE", None),
155
- getattr(ReportAction, "CREATE", None),
156
- )
157
- style = (
158
- "orange1"
159
- if getattr(event, "action", None) in modification_actions
160
- else "cyan"
161
- )
162
- self.console.print(Text(msg, style=style), end="")
163
- self.console.file.flush()
164
- elif subtype in (
165
- ReportSubtype.SUCCESS,
166
- ReportSubtype.ERROR,
167
- ReportSubtype.WARNING,
168
- ):
169
- self.console.print(msg)
170
- self.console.file.flush()
171
- elif subtype == ReportSubtype.STDOUT:
172
- self.console.print(msg)
173
- self.console.file.flush()
174
- elif subtype == ReportSubtype.STDERR:
175
- self.console.print(Text(msg, style="on red"))
176
- self.console.file.flush()
177
- else:
178
- self.console.print(msg)
179
- self.console.file.flush()
1
+ from rich.console import Console
2
+ from rich.markdown import Markdown
3
+ from rich.pretty import Pretty
4
+ from rich.panel import Panel
5
+ from rich.text import Text
6
+ from janito.event_bus.handler import EventHandlerBase
7
+ import janito.driver_events as driver_events
8
+ from janito.report_events import ReportSubtype, ReportAction
9
+ from janito.event_bus.bus import event_bus
10
+ from janito.llm import message_parts
11
+ import janito.agent_events as agent_events
12
+
13
+
14
+ import sys
15
+
16
+
17
+ class RichTerminalReporter(EventHandlerBase):
18
+ """
19
+ Handles UI rendering for janito events using Rich.
20
+
21
+ - For ResponseReceived events, iterates over the 'parts' field and displays each part appropriately:
22
+ - TextMessagePart: rendered as Markdown (uses 'content' field)
23
+ - Other MessageParts: displayed using Pretty or a suitable Rich representation
24
+ - For RequestFinished events, output is printed only if raw mode is enabled (using Pretty formatting).
25
+ - Report events (info, success, error, etc.) are always printed with appropriate styling.
26
+ """
27
+
28
+ def __init__(self, raw_mode=False):
29
+ from janito.cli.console import shared_console
30
+
31
+ self.console = shared_console
32
+ self.raw_mode = raw_mode
33
+ import janito.report_events as report_events
34
+
35
+ import janito.tools.tool_events as tool_events
36
+
37
+ super().__init__(driver_events, report_events, tool_events, agent_events)
38
+ self._waiting_printed = False
39
+
40
+ def on_RequestStarted(self, event):
41
+ # Print waiting message with provider and model name
42
+ provider = None
43
+ model = None
44
+ if hasattr(event, "payload") and isinstance(event.payload, dict):
45
+ provider = event.payload.get("provider_name")
46
+ model = event.payload.get("model") or event.payload.get("model_name")
47
+ if not provider:
48
+ provider = getattr(event, "provider_name", None)
49
+ if not provider:
50
+ provider = getattr(event, "driver_name", None)
51
+ if not provider:
52
+ provider = "LLM"
53
+ if not model:
54
+ model = getattr(event, "model", None)
55
+ if not model:
56
+ model = getattr(event, "model_name", None)
57
+ if not model:
58
+ model = "?"
59
+ self.console.print(
60
+ f"[bold cyan]Waiting for {provider} (model: {model})...[/bold cyan]", end=""
61
+ )
62
+ self._waiting_printed = True
63
+
64
+ def on_AgentWaitingForResponse(self, event):
65
+ # Agent waiting - set flag but don't print anything
66
+ self._waiting_printed = True
67
+
68
+ def on_ResponseReceived(self, event):
69
+ parts = event.parts if hasattr(event, "parts") else None
70
+ if not parts:
71
+ self.console.print("[No response parts to display]")
72
+ self.console.file.flush()
73
+ return
74
+ for part in parts:
75
+ if isinstance(part, message_parts.TextMessagePart):
76
+ self.console.print(Markdown(part.content))
77
+ self.console.file.flush()
78
+
79
+ def delete_current_line(self):
80
+ """
81
+ Clears the entire current line in the terminal and returns the cursor to column 1.
82
+ """
83
+ # Use raw ANSI escape sequences but write directly to the underlying file
84
+ # to bypass Rich's escaping/interpretation
85
+ if hasattr(self.console, 'file') and hasattr(self.console.file, 'write'):
86
+ self.console.file.write("\r\033[2K")
87
+ self.console.file.flush()
88
+ else:
89
+ # Fallback to sys.stdout if console.file is not available
90
+ import sys
91
+ sys.stdout.write("\r\033[2K")
92
+ sys.stdout.flush()
93
+
94
+ def on_RequestFinished(self, event):
95
+ if self._waiting_printed:
96
+ self.delete_current_line()
97
+ self._waiting_printed = False
98
+
99
+ def on_AgentReceivedResponse(self, event):
100
+ # Clear any waiting message when agent receives response
101
+ if self._waiting_printed:
102
+ self.delete_current_line()
103
+ self._waiting_printed = False
104
+
105
+ def on_ToolCallError(self, event):
106
+ # Optionally handle tool call errors in a user-friendly way
107
+ error = getattr(event, "error", None)
108
+ tool = getattr(event, "tool_name", None)
109
+ if error and tool:
110
+ self.console.print(f"[bold red]Tool Error ({tool}):[/] {error}")
111
+ self.console.file.flush()
112
+
113
+ def on_ReportEvent(self, event):
114
+ # Special handling for security-related report events
115
+ subtype = getattr(event, "subtype", None)
116
+ msg = getattr(event, "message", None)
117
+ action = getattr(event, "action", None)
118
+ tool = getattr(event, "tool", None)
119
+ context = getattr(event, "context", None)
120
+ if (
121
+ subtype == ReportSubtype.ERROR
122
+ and msg
123
+ and "[SECURITY] Path access denied" in msg
124
+ ):
125
+ # Highlight security errors with a distinct style
126
+ self.console.print(
127
+ Panel(f"{msg}", title="[red]SECURITY VIOLATION[/red]", style="bold red")
128
+ )
129
+ self.console.file.flush()
130
+ return
131
+
132
+ msg = event.message if hasattr(event, "message") else None
133
+ subtype = event.subtype if hasattr(event, "subtype") else None
134
+ if not msg or not subtype:
135
+ return
136
+ if subtype == ReportSubtype.ACTION_INFO:
137
+ # Clear any waiting message before showing action info
138
+ if self._waiting_printed:
139
+ self.delete_current_line()
140
+ self._waiting_printed = False
141
+ # Use orange for all write/modification actions
142
+ modification_actions = (
143
+ getattr(ReportAction, "UPDATE", None),
144
+ getattr(ReportAction, "WRITE", None),
145
+ getattr(ReportAction, "DELETE", None),
146
+ getattr(ReportAction, "CREATE", None),
147
+ )
148
+ style = (
149
+ "orange1"
150
+ if getattr(event, "action", None) in modification_actions
151
+ else "cyan"
152
+ )
153
+ self.console.print(Text(msg, style=style), end="")
154
+ self.console.file.flush()
155
+ elif subtype in (
156
+ ReportSubtype.SUCCESS,
157
+ ReportSubtype.ERROR,
158
+ ReportSubtype.WARNING,
159
+ ):
160
+ self.console.print(msg)
161
+ self.console.file.flush()
162
+ elif subtype == ReportSubtype.STDOUT:
163
+ self.console.print(msg)
164
+ self.console.file.flush()
165
+ elif subtype == ReportSubtype.STDERR:
166
+ self.console.print(Text(msg, style="on red"))
167
+ self.console.file.flush()
168
+ else:
169
+ self.console.print(msg)
170
+ self.console.file.flush()
@@ -45,6 +45,25 @@ class PromptHandler:
45
45
  def handle(self) -> None:
46
46
  import traceback
47
47
 
48
+ # Check if interactive mode is requested - if so, switch to chat mode
49
+ if getattr(self.args, "interactive", False):
50
+ from janito.cli.chat_mode.session import ChatSession
51
+ from rich.console import Console
52
+
53
+ console = Console()
54
+ session = ChatSession(
55
+ console,
56
+ self.provider_instance,
57
+ self.llm_driver_config,
58
+ role=self.role,
59
+ args=self.args,
60
+ verbose_tools=getattr(self.args, "verbose_tools", False),
61
+ verbose_agent=getattr(self.args, "verbose_agent", False),
62
+ allowed_permissions=getattr(self, 'allowed_permissions', None),
63
+ )
64
+ session.run()
65
+ return
66
+
48
67
  user_prompt = " ".join(getattr(self.args, "user_prompt", [])).strip()
49
68
  # UTF-8 sanitize user_prompt
50
69
  sanitized = user_prompt
janito/llm/agent.py CHANGED
@@ -4,6 +4,17 @@ from janito.conversation_history import LLMConversationHistory
4
4
  from janito.tools.tools_adapter import ToolsAdapterBase
5
5
  from queue import Queue, Empty
6
6
  from janito.driver_events import RequestStatus
7
+ from janito.agent_events import (
8
+ AgentInitialized,
9
+ AgentChatStarted,
10
+ AgentChatFinished,
11
+ AgentProcessingResponse,
12
+ AgentToolCallStarted,
13
+ AgentToolCallFinished,
14
+ AgentWaitingForResponse,
15
+ AgentReceivedResponse,
16
+ AgentShutdown
17
+ )
7
18
  from typing import Any, Optional, List, Iterator, Union
8
19
  import threading
9
20
  import logging
@@ -53,6 +64,9 @@ class LLMAgent:
53
64
  self._latest_event = None
54
65
  self.verbose_agent = verbose_agent
55
66
  self.driver = None # Will be set by setup_agent if available
67
+
68
+ # Emit agent initialized event
69
+ event_bus.publish(AgentInitialized(agent_name=self.agent_name))
56
70
 
57
71
  def get_provider_name(self):
58
72
  # Try to get provider name from driver, fallback to llm_provider, else '?'
@@ -178,6 +192,9 @@ class LLMAgent:
178
192
  Wait for a single event from the output queue (with timeout), process it, and return the result.
179
193
  This function is intended to be called from the main agent loop, which controls the overall flow.
180
194
  """
195
+ # Emit agent waiting for response event
196
+ event_bus.publish(AgentWaitingForResponse(agent_name=self.agent_name))
197
+
181
198
  if getattr(self, "verbose_agent", False):
182
199
  print("[agent] [DEBUG] Entered _process_next_response")
183
200
  elapsed = 0.0
@@ -204,6 +221,10 @@ class LLMAgent:
204
221
  if getattr(self, "verbose_agent", False):
205
222
  print(f"[agent] [DEBUG] Waiting for LLM response... ({elapsed:.1f}s elapsed)")
206
223
  continue
224
+
225
+ # Emit agent received response event
226
+ event_bus.publish(AgentReceivedResponse(agent_name=self.agent_name, response=event))
227
+
207
228
  if getattr(self, "verbose_agent", False):
208
229
  print(f"[agent] [DEBUG] Received event from output_queue: {event}")
209
230
  event_bus.publish(event)
@@ -233,6 +254,10 @@ class LLMAgent:
233
254
  """
234
255
  if getattr(self, "verbose_agent", False):
235
256
  print("[agent] [INFO] Handling ResponseReceived event.")
257
+
258
+ # Emit agent processing response event
259
+ event_bus.publish(AgentProcessingResponse(agent_name=self.agent_name, response=event))
260
+
236
261
  from janito.llm.message_parts import FunctionCallMessagePart
237
262
 
238
263
  # Skip tool processing if no tools adapter is available
@@ -249,6 +274,15 @@ class LLMAgent:
249
274
  print(
250
275
  f"[agent] [DEBUG] Tool call detected: {getattr(part, 'name', repr(part))} with arguments: {getattr(part, 'arguments', None)}"
251
276
  )
277
+
278
+ # Emit agent tool call started event
279
+ event_bus.publish(AgentToolCallStarted(
280
+ agent_name=self.agent_name,
281
+ tool_call_id=getattr(part, 'tool_call_id', None),
282
+ name=getattr(part, 'name', None),
283
+ arguments=getattr(part, 'arguments', None)
284
+ ))
285
+
252
286
  tool_calls.append(part)
253
287
  try:
254
288
  result = self.tools_adapter.execute_function_call_message_part(part)
@@ -257,6 +291,14 @@ class LLMAgent:
257
291
  # instead of letting it propagate to the user
258
292
  result = str(e)
259
293
  tool_results.append(result)
294
+
295
+ # Emit agent tool call finished event
296
+ event_bus.publish(AgentToolCallFinished(
297
+ agent_name=self.agent_name,
298
+ tool_call_id=getattr(part, 'tool_call_id', None),
299
+ name=getattr(part, 'name', None),
300
+ result=result
301
+ ))
260
302
  if tool_calls:
261
303
  # Prepare tool_calls message for assistant
262
304
  tool_calls_list = []
@@ -316,6 +358,14 @@ class LLMAgent:
316
358
  role: str = "user",
317
359
  config=None,
318
360
  ):
361
+ # Emit agent chat started event
362
+ event_bus.publish(AgentChatStarted(
363
+ agent_name=self.agent_name,
364
+ prompt=prompt,
365
+ messages=messages,
366
+ role=role
367
+ ))
368
+
319
369
  self._clear_driver_queues()
320
370
  self._validate_and_update_history(prompt, messages, role)
321
371
  self._ensure_system_prompt()
@@ -339,6 +389,12 @@ class LLMAgent:
339
389
  f"[agent] [DEBUG] Returned from _process_next_response: result={result}, added_tool_results={added_tool_results}"
340
390
  )
341
391
  if self._should_exit_chat_loop(result, added_tool_results):
392
+ # Emit agent chat finished event
393
+ event_bus.publish(AgentChatFinished(
394
+ agent_name=self.agent_name,
395
+ result=result,
396
+ loop_count=loop_count
397
+ ))
342
398
  return result
343
399
  loop_count += 1
344
400
 
@@ -502,6 +558,9 @@ class LLMAgent:
502
558
  :param timeout: Optional timeout in seconds.
503
559
  Handles KeyboardInterrupt gracefully.
504
560
  """
561
+ # Emit agent shutdown event
562
+ event_bus.publish(AgentShutdown(agent_name=self.agent_name))
563
+
505
564
  if (
506
565
  hasattr(self, "driver")
507
566
  and self.driver
@@ -30,6 +30,7 @@ from .show_image_grid import ShowImageGridTool
30
30
  from janito.tools.tool_base import ToolPermissions
31
31
  import os
32
32
  from janito.tools.permissions import get_global_allowed_permissions
33
+ from janito.platform_discovery import PlatformDiscovery
33
34
 
34
35
  # Singleton tools adapter with all standard tools registered
35
36
  local_tools_adapter = LocalToolsAdapter(workdir=os.getcwd())
@@ -40,6 +41,9 @@ def get_local_tools_adapter(workdir=None):
40
41
 
41
42
 
42
43
  # Register tools
44
+ pd = PlatformDiscovery()
45
+ is_powershell = pd.detect_shell().startswith("PowerShell")
46
+
43
47
  for tool_class in [
44
48
  AskUserTool,
45
49
  CopyFileTool,
@@ -68,6 +72,9 @@ for tool_class in [
68
72
  ShowImageTool,
69
73
  ShowImageGridTool,
70
74
  ]:
75
+ # Skip bash tools when running in PowerShell
76
+ if is_powershell and tool_class.__name__ in ["RunBashCommandTool"]:
77
+ continue
71
78
  local_tools_adapter.register_tool(tool_class)
72
79
 
73
80
  # DEBUG: Print registered tools at startup
@@ -6,6 +6,7 @@ from janito.report_events import ReportAction
6
6
  from janito.i18n import tr
7
7
  import os
8
8
  from janito.tools.path_utils import expand_path
9
+ from pathlib import Path
9
10
 
10
11
 
11
12
  @register_local_tool
@@ -43,6 +44,8 @@ class CreateDirectoryTool(ToolBase):
43
44
  "❌ Path '{disp_path}' exists and is not a directory.",
44
45
  disp_path=disp_path,
45
46
  )
47
+ # Generate content summary
48
+ content_summary = self._get_directory_summary(path)
46
49
  self.report_error(
47
50
  tr(
48
51
  "❗ Directory '{disp_path}' already exists.",
@@ -50,8 +53,9 @@ class CreateDirectoryTool(ToolBase):
50
53
  )
51
54
  )
52
55
  return tr(
53
- "❗ Cannot create directory: '{disp_path}' already exists.",
56
+ "❗ Cannot create directory: '{disp_path}' already exists.\n{summary}",
54
57
  disp_path=disp_path,
58
+ summary=content_summary,
55
59
  )
56
60
  os.makedirs(path, exist_ok=True)
57
61
  self.report_success(tr("✅ Directory created"))
@@ -68,3 +72,42 @@ class CreateDirectoryTool(ToolBase):
68
72
  )
69
73
  )
70
74
  return tr("❌ Cannot create directory: {error}", error=e)
75
+
76
+ def _get_directory_summary(self, path: str) -> str:
77
+ """Generate a summary of directory contents."""
78
+ try:
79
+ path_obj = Path(path)
80
+ if not path_obj.exists() or not path_obj.is_dir():
81
+ return ""
82
+
83
+ items = list(path_obj.iterdir())
84
+ if not items:
85
+ return "Directory is empty."
86
+
87
+ # Count files and directories
88
+ file_count = sum(1 for item in items if item.is_file())
89
+ dir_count = sum(1 for item in items if item.is_dir())
90
+
91
+ summary_parts = []
92
+ if file_count > 0:
93
+ summary_parts.append(f"{file_count} file{'s' if file_count != 1 else ''}")
94
+ if dir_count > 0:
95
+ summary_parts.append(f"{dir_count} subdirector{'y' if dir_count == 1 else 'ies'}")
96
+
97
+ # Show first few items as examples
98
+ examples = []
99
+ for item in sorted(items)[:3]: # Show up to 3 items
100
+ if item.is_dir():
101
+ examples.append(f"📁 {item.name}")
102
+ else:
103
+ examples.append(f"📄 {item.name}")
104
+
105
+ result = f"Contains: {', '.join(summary_parts)}."
106
+ if examples:
107
+ result += f"\nExamples: {', '.join(examples)}"
108
+ if len(items) > 3:
109
+ result += f" (and {len(items) - 3} more)"
110
+
111
+ return result
112
+ except Exception:
113
+ return "Unable to read directory contents."