janito 3.10.0__py3-none-any.whl → 3.12.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/cli/prompt_core.py CHANGED
@@ -1,301 +1,336 @@
1
- """
2
- Core PromptHandler: Handles prompt submission and response formatting for janito CLI (shared by single and chat modes).
3
- """
4
-
5
- import time
6
- from janito import __version__ as VERSION
7
- from janito.performance_collector import PerformanceCollector
8
- from rich.status import Status
9
- from rich.console import Console
10
- from typing import Any, Optional, Callable
11
- from janito.driver_events import (
12
- RequestStarted,
13
- RequestFinished,
14
- RequestStatus,
15
- RateLimitRetry,
16
- )
17
- from janito.tools.tool_events import ToolCallError, ToolCallStarted
18
- import threading
19
- from janito.cli.verbose_output import print_verbose_header
20
- from janito.event_bus import event_bus as global_event_bus
21
-
22
-
23
- class StatusRef:
24
- def __init__(self):
25
- self.status = None
26
-
27
-
28
- class PromptHandler:
29
- args: Any
30
- agent: Any
31
- performance_collector: PerformanceCollector
32
- console: Console
33
- provider_instance: Any
34
-
35
- def __init__(self, args: Any, conversation_history, provider_instance) -> None:
36
- self.temperature = args.temperature if hasattr(args, "temperature") else None
37
- """
38
- Initialize PromptHandler.
39
- :param args: CLI or programmatic arguments for provider/model selection, etc.
40
- :param conversation_history: LLMConversationHistory object for multi-turn chat mode.
41
- :param provider_instance: An initialized provider instance.
42
- """
43
- self.args = args
44
- self.conversation_history = conversation_history
45
- self.provider_instance = provider_instance
46
- self.agent = None
47
- from janito.perf_singleton import performance_collector
48
-
49
- self.performance_collector = performance_collector
50
- self.console = Console()
51
-
52
- def _handle_inner_event(self, inner_event, on_event, status):
53
- if on_event:
54
- on_event(inner_event)
55
- from janito.tools.tool_events import ToolCallFinished, ToolCallStarted
56
-
57
- if isinstance(inner_event, ToolCallStarted):
58
- return self._handle_tool_call_started(inner_event, status)
59
- if isinstance(inner_event, ToolCallFinished):
60
- return self._handle_tool_call_finished(inner_event)
61
- if isinstance(inner_event, RateLimitRetry):
62
- return self._handle_rate_limit_retry(inner_event, status)
63
- if isinstance(inner_event, RequestFinished):
64
- if getattr(inner_event, "status", None) == "error":
65
- return self._handle_request_finished_error(inner_event, status)
66
- if getattr(inner_event, "status", None) in (
67
- RequestStatus.EMPTY_RESPONSE,
68
- RequestStatus.TIMEOUT,
69
- ):
70
- return self._handle_empty_or_timeout(inner_event, status)
71
- status.update("[bold green]Received response![bold green]")
72
- return "break"
73
- if isinstance(inner_event, ToolCallError):
74
- return self._handle_tool_call_error(inner_event, status)
75
- event_type = type(inner_event).__name__
76
- self.console.print(
77
- f"[yellow]Warning: Unknown event type encountered: {event_type}[yellow]"
78
- )
79
- return None
80
-
81
- def _handle_tool_call_started(self, inner_event, status):
82
- """Handle ToolCallStarted event - clear the status before any tool execution."""
83
- # Always clear the status when any tool starts to avoid cluttering the UI
84
- if status:
85
- status.update("")
86
- return None
87
-
88
- def _handle_tool_call_finished(self, inner_event):
89
- if hasattr(self.args, "verbose_tools") and self.args.verbose_tools:
90
- self.console.print(
91
- f"[cyan][tools-adapter] Tool '{inner_event.tool_name}' result:[/cyan] {inner_event.result}"
92
- )
93
- else:
94
- self.console.print(inner_event.result)
95
- return None
96
-
97
- def _handle_rate_limit_retry(self, inner_event, status):
98
- status.update(
99
- f"[yellow]Rate limited. Waiting {inner_event.retry_delay:.0f}s before retry (attempt {inner_event.attempt}).[yellow]"
100
- )
101
- return None
102
-
103
- def _handle_request_finished_error(self, inner_event, status):
104
- error_msg = (
105
- inner_event.error if hasattr(inner_event, "error") else "Unknown error"
106
- )
107
- if (
108
- "Status 429" in error_msg
109
- and "Service tier capacity exceeded for this model" in error_msg
110
- ):
111
- status.update("[yellow]Service tier capacity exceeded, retrying...[yellow]")
112
- return "break"
113
- status.update(f"[bold red]Error: {error_msg}[bold red]")
114
- self.console.print(f"[red]Error: {error_msg}[red]")
115
- return "break"
116
-
117
- def _handle_tool_call_error(self, inner_event, status):
118
- error_msg = (
119
- inner_event.error if hasattr(inner_event, "error") else "Unknown tool error"
120
- )
121
- tool_name = (
122
- inner_event.tool_name if hasattr(inner_event, "tool_name") else "unknown"
123
- )
124
- status.update(f"[bold red]Tool Error in '{tool_name}': {error_msg}[bold red]")
125
- self.console.print(f"[red]Tool Error in '{tool_name}': {error_msg}[red]")
126
- return "break"
127
-
128
- def _handle_empty_or_timeout(self, inner_event, status):
129
- details = getattr(inner_event, "details", None) or {}
130
- block_reason = details.get("block_reason")
131
- block_msg = details.get("block_reason_message")
132
- msg = details.get("message", "LLM returned an empty or incomplete response.")
133
- driver_name = getattr(inner_event, "driver_name", "unknown driver")
134
- if block_reason or block_msg:
135
- status.update(
136
- f"[bold yellow]Blocked by driver: {driver_name} | {block_reason or ''} {block_msg or ''}[bold yellow]"
137
- )
138
- self.console.print(
139
- f"[yellow]Blocked by driver: {driver_name} (empty response): {block_reason or ''}\n{block_msg or ''}[/yellow]"
140
- )
141
- else:
142
- status.update(
143
- f"[yellow]LLM produced no output for this request (driver: {driver_name}).[/yellow]"
144
- )
145
- self.console.print(
146
- f"[yellow]Warning: {msg} (driver: {driver_name})[/yellow]"
147
- )
148
- return "break"
149
-
150
- def _process_event_iter(self, event_iter, on_event):
151
- for event in event_iter:
152
- # Handle exceptions from generation thread
153
- if isinstance(event, dict) and event.get("type") == "exception":
154
- self.console.print("[red]Exception in generation thread:[red]")
155
- self.console.print(event.get("traceback", "No traceback available"))
156
- break
157
- if on_event:
158
- on_event(event)
159
- if isinstance(event, RequestStarted):
160
- pass # No change needed for started event
161
- elif isinstance(event, RequestFinished) and getattr(
162
- event, "status", None
163
- ) in ("error", "cancelled"):
164
- # Handle error/cancelled as needed
165
- for inner_event in event_iter:
166
- result = self._handle_inner_event(inner_event, on_event, None)
167
- if result == "break":
168
- break
169
- # After exiting, continue with next events (if any)
170
- # Handle other event types outside the spinner if needed
171
- elif isinstance(event, RequestFinished) and getattr(
172
- event, "status", None
173
- ) in (RequestStatus.EMPTY_RESPONSE, RequestStatus.TIMEOUT):
174
- details = getattr(event, "details", None) or {}
175
- block_reason = details.get("block_reason")
176
- block_msg = details.get("block_reason_message")
177
- msg = details.get(
178
- "message", "LLM returned an empty or incomplete response."
179
- )
180
- driver_name = getattr(event, "driver_name", "unknown driver")
181
- if block_reason or block_msg:
182
- self.console.print(
183
- f"[yellow]Blocked by driver: {driver_name} (empty response): {block_reason or ''}\n{block_msg or ''}[/yellow]"
184
- )
185
- else:
186
- self.console.print(
187
- f"[yellow]Warning: {msg} (driver: {driver_name})[/yellow]"
188
- )
189
- else:
190
- pass
191
-
192
- def handle_prompt(
193
- self, user_prompt, args=None, print_header=True, raw=False, on_event=None
194
- ):
195
- # args defaults to self.args for compatibility in interactive mode
196
- args = (
197
- args if args is not None else self.args if hasattr(self, "args") else None
198
- )
199
- # Join/cleanup prompt
200
- if isinstance(user_prompt, list):
201
- user_prompt = " ".join(user_prompt).strip()
202
- else:
203
- user_prompt = str(user_prompt).strip() if user_prompt is not None else ""
204
- if not user_prompt:
205
- raise ValueError("No user prompt was provided!")
206
- if print_header and hasattr(self, "agent") and args is not None:
207
- print_verbose_header(self.agent, args)
208
- self.run_prompt(user_prompt, raw=raw, on_event=on_event)
209
-
210
- def run_prompt(
211
- self, user_prompt: str, raw: bool = False, on_event: Optional[Callable] = None
212
- ) -> None:
213
- """
214
- Handles a single prompt, using the blocking event-driven chat interface.
215
- Optionally takes an on_event callback for custom event handling.
216
- """
217
- try:
218
- self._print_verbose_debug("Calling agent.chat()...")
219
-
220
- # Show waiting status with elapsed time
221
- start_time = time.time()
222
-
223
- # Get provider and model info for status display
224
- provider_name = self.agent.get_provider_name() if hasattr(self.agent, 'get_provider_name') else 'LLM'
225
- model_name = self.agent.get_model_name() if hasattr(self.agent, 'get_model_name') else 'unknown'
226
-
227
- status = Status(f"[bold blue]Waiting for {provider_name} (model: {model_name})...[/bold blue]")
228
-
229
- # Thread coordination event
230
- stop_updater = threading.Event()
231
-
232
- def update_status():
233
- elapsed = time.time() - start_time
234
- status.update(f"[bold blue]Waiting for {provider_name} (model: {model_name})... ({elapsed:.1f}s)[/bold blue]")
235
-
236
- # Start status display and update timer
237
- with status:
238
- # Update status every second in a separate thread
239
- def status_updater():
240
- while not stop_updater.is_set():
241
- update_status()
242
- stop_updater.wait(1.0) # Wait for 1 second or until stopped
243
-
244
- updater_thread = threading.Thread(target=status_updater, daemon=True)
245
- updater_thread.start()
246
-
247
- try:
248
- final_event = self.agent.chat(prompt=user_prompt)
249
- finally:
250
- # Signal the updater thread to stop
251
- stop_updater.set()
252
- # Wait a bit for the thread to clean up
253
- updater_thread.join(timeout=0.1)
254
-
255
- if hasattr(self.agent, "set_latest_event"):
256
- self.agent.set_latest_event(final_event)
257
- self.agent.last_event = final_event
258
- self._print_verbose_debug(f"agent.chat() returned: {final_event}")
259
- self._print_verbose_final_event(final_event)
260
- if on_event and final_event is not None:
261
- on_event(final_event)
262
- global_event_bus.publish(final_event)
263
- except KeyboardInterrupt:
264
- # Capture user interrupt / cancellation
265
- self.console.print("[red]Interrupted by the user.[/red]")
266
- try:
267
- from janito.driver_events import RequestFinished, RequestStatus
268
-
269
- # Record a synthetic "cancelled" final event so that downstream
270
- # handlers (e.g. single_shot_mode.handler._post_prompt_actions)
271
- # can reliably detect that the prompt was interrupted by the
272
- # user and avoid showing misleading messages such as
273
- # "No output produced by the model.".
274
- if hasattr(self, "agent") and self.agent is not None:
275
- self.agent.last_event = RequestFinished(
276
- status=RequestStatus.CANCELLED,
277
- reason="Interrupted by the user",
278
- )
279
- except Exception:
280
- # Do not fail on cleanup – this hook is best-effort only.
281
- pass
282
-
283
- def _print_verbose_debug(self, message):
284
- if hasattr(self.args, "verbose_agent") and self.args.verbose_agent:
285
- print(f"[prompt_core][DEBUG] {message}")
286
-
287
- def _print_verbose_final_event(self, final_event):
288
- if hasattr(self.args, "verbose_agent") and self.args.verbose_agent:
289
- print("[prompt_core][DEBUG] Received final_event from agent.chat:")
290
- print(f" [prompt_core][DEBUG] type={type(final_event)}")
291
- print(f" [prompt_core][DEBUG] content={final_event}")
292
-
293
- def run_prompts(
294
- self, prompts: list, raw: bool = False, on_event: Optional[Callable] = None
295
- ) -> None:
296
- """
297
- Handles multiple prompts in sequence, collecting performance data for each.
298
- """
299
- for prompt in prompts:
300
- self.run_prompt(prompt, raw=raw, on_event=on_event)
301
- # No return value
1
+ """
2
+ Core PromptHandler: Handles prompt submission and response formatting for janito CLI (shared by single and chat modes).
3
+ """
4
+
5
+ import time
6
+ import sys
7
+ from janito import __version__ as VERSION
8
+ from janito.performance_collector import PerformanceCollector
9
+ from rich.status import Status
10
+ from rich.console import Console
11
+ from typing import Any, Optional, Callable
12
+ from janito.driver_events import (
13
+ RequestStarted,
14
+ RequestFinished,
15
+ RequestStatus,
16
+ RateLimitRetry,
17
+ )
18
+ from janito.tools.tool_events import ToolCallError, ToolCallStarted
19
+ import threading
20
+ from janito.cli.verbose_output import print_verbose_header
21
+ from janito.event_bus import event_bus as global_event_bus
22
+
23
+
24
+ class StatusRef:
25
+ def __init__(self):
26
+ self.status = None
27
+
28
+
29
+ class PromptHandler:
30
+ args: Any
31
+ agent: Any
32
+ performance_collector: PerformanceCollector
33
+ console: Console
34
+ provider_instance: Any
35
+
36
+ def __init__(self, args: Any, conversation_history, provider_instance) -> None:
37
+ self.temperature = args.temperature if hasattr(args, "temperature") else None
38
+ """
39
+ Initialize PromptHandler.
40
+ :param args: CLI or programmatic arguments for provider/model selection, etc.
41
+ :param conversation_history: LLMConversationHistory object for multi-turn chat mode.
42
+ :param provider_instance: An initialized provider instance.
43
+ """
44
+ self.args = args
45
+ self.conversation_history = conversation_history
46
+ self.provider_instance = provider_instance
47
+ self.agent = None
48
+ from janito.perf_singleton import performance_collector
49
+
50
+ self.performance_collector = performance_collector
51
+ self.console = Console()
52
+
53
+ def _handle_inner_event(self, inner_event, on_event, status):
54
+ if on_event:
55
+ on_event(inner_event)
56
+ from janito.tools.tool_events import ToolCallFinished, ToolCallStarted
57
+
58
+ if isinstance(inner_event, ToolCallStarted):
59
+ return self._handle_tool_call_started(inner_event, status)
60
+ if isinstance(inner_event, ToolCallFinished):
61
+ return self._handle_tool_call_finished(inner_event)
62
+ if isinstance(inner_event, RateLimitRetry):
63
+ return self._handle_rate_limit_retry(inner_event, status)
64
+ if isinstance(inner_event, RequestFinished):
65
+ if getattr(inner_event, "status", None) == "error":
66
+ return self._handle_request_finished_error(inner_event, status)
67
+ if getattr(inner_event, "status", None) in (
68
+ RequestStatus.EMPTY_RESPONSE,
69
+ RequestStatus.TIMEOUT,
70
+ ):
71
+ return self._handle_empty_or_timeout(inner_event, status)
72
+ status.update("[bold green]Received response![bold green]")
73
+ return "break"
74
+ if isinstance(inner_event, ToolCallError):
75
+ return self._handle_tool_call_error(inner_event, status)
76
+ event_type = type(inner_event).__name__
77
+ self.console.print(
78
+ f"[yellow]Warning: Unknown event type encountered: {event_type}[yellow]"
79
+ )
80
+ return None
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
+
96
+ def _handle_tool_call_started(self, inner_event, status):
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()
103
+ return None
104
+
105
+ def _handle_tool_call_finished(self, inner_event):
106
+ if hasattr(self.args, "verbose_tools") and self.args.verbose_tools:
107
+ self.console.print(
108
+ f"[cyan][tools-adapter] Tool '{inner_event.tool_name}' result:[/cyan] {inner_event.result}"
109
+ )
110
+ else:
111
+ self.console.print(inner_event.result)
112
+ return None
113
+
114
+ def _handle_rate_limit_retry(self, inner_event, status):
115
+ status.update(
116
+ f"[yellow]Rate limited. Waiting {inner_event.retry_delay:.0f}s before retry (attempt {inner_event.attempt}).[yellow]"
117
+ )
118
+ return None
119
+
120
+ def _handle_request_finished_error(self, inner_event, status):
121
+ error_msg = (
122
+ inner_event.error if hasattr(inner_event, "error") else "Unknown error"
123
+ )
124
+ if (
125
+ "Status 429" in error_msg
126
+ and "Service tier capacity exceeded for this model" in error_msg
127
+ ):
128
+ status.update("[yellow]Service tier capacity exceeded, retrying...[yellow]")
129
+ return "break"
130
+ status.update(f"[bold red]Error: {error_msg}[bold red]")
131
+ self.console.print(f"[red]Error: {error_msg}[red]")
132
+ return "break"
133
+
134
+ def _handle_tool_call_error(self, inner_event, status):
135
+ error_msg = (
136
+ inner_event.error if hasattr(inner_event, "error") else "Unknown tool error"
137
+ )
138
+ tool_name = (
139
+ inner_event.tool_name if hasattr(inner_event, "tool_name") else "unknown"
140
+ )
141
+ status.update(f"[bold red]Tool Error in '{tool_name}': {error_msg}[bold red]")
142
+ self.console.print(f"[red]Tool Error in '{tool_name}': {error_msg}[red]")
143
+ return "break"
144
+
145
+ def _handle_empty_or_timeout(self, inner_event, status):
146
+ details = getattr(inner_event, "details", None) or {}
147
+ block_reason = details.get("block_reason")
148
+ block_msg = details.get("block_reason_message")
149
+ msg = details.get("message", "LLM returned an empty or incomplete response.")
150
+ driver_name = getattr(inner_event, "driver_name", "unknown driver")
151
+ if block_reason or block_msg:
152
+ status.update(
153
+ f"[bold yellow]Blocked by driver: {driver_name} | {block_reason or ''} {block_msg or ''}[bold yellow]"
154
+ )
155
+ self.console.print(
156
+ f"[yellow]Blocked by driver: {driver_name} (empty response): {block_reason or ''}\n{block_msg or ''}[/yellow]"
157
+ )
158
+ else:
159
+ status.update(
160
+ f"[yellow]LLM produced no output for this request (driver: {driver_name}).[/yellow]"
161
+ )
162
+ self.console.print(
163
+ f"[yellow]Warning: {msg} (driver: {driver_name})[/yellow]"
164
+ )
165
+ return "break"
166
+
167
+ def _process_event_iter(self, event_iter, on_event):
168
+ for event in event_iter:
169
+ # Handle exceptions from generation thread
170
+ if isinstance(event, dict) and event.get("type") == "exception":
171
+ self.console.print("[red]Exception in generation thread:[red]")
172
+ self.console.print(event.get("traceback", "No traceback available"))
173
+ break
174
+ if on_event:
175
+ on_event(event)
176
+ if isinstance(event, RequestStarted):
177
+ pass # No change needed for started event
178
+ elif isinstance(event, RequestFinished) and getattr(
179
+ event, "status", None
180
+ ) in ("error", "cancelled"):
181
+ # Handle error/cancelled as needed
182
+ for inner_event in event_iter:
183
+ result = self._handle_inner_event(inner_event, on_event, None)
184
+ if result == "break":
185
+ break
186
+ # After exiting, continue with next events (if any)
187
+ # Handle other event types outside the spinner if needed
188
+ elif isinstance(event, RequestFinished) and getattr(
189
+ event, "status", None
190
+ ) in (RequestStatus.EMPTY_RESPONSE, RequestStatus.TIMEOUT):
191
+ details = getattr(event, "details", None) or {}
192
+ block_reason = details.get("block_reason")
193
+ block_msg = details.get("block_reason_message")
194
+ msg = details.get(
195
+ "message", "LLM returned an empty or incomplete response."
196
+ )
197
+ driver_name = getattr(event, "driver_name", "unknown driver")
198
+ if block_reason or block_msg:
199
+ self.console.print(
200
+ f"[yellow]Blocked by driver: {driver_name} (empty response): {block_reason or ''}\n{block_msg or ''}[/yellow]"
201
+ )
202
+ else:
203
+ self.console.print(
204
+ f"[yellow]Warning: {msg} (driver: {driver_name})[/yellow]"
205
+ )
206
+ else:
207
+ pass
208
+
209
+ def handle_prompt(
210
+ self, user_prompt, args=None, print_header=True, raw=False, on_event=None
211
+ ):
212
+ # args defaults to self.args for compatibility in interactive mode
213
+ args = (
214
+ args if args is not None else self.args if hasattr(self, "args") else None
215
+ )
216
+ # Join/cleanup prompt
217
+ if isinstance(user_prompt, list):
218
+ user_prompt = " ".join(user_prompt).strip()
219
+ else:
220
+ user_prompt = str(user_prompt).strip() if user_prompt is not None else ""
221
+ if not user_prompt:
222
+ raise ValueError("No user prompt was provided!")
223
+ if print_header and hasattr(self, "agent") and args is not None:
224
+ print_verbose_header(self.agent, args)
225
+ self.run_prompt(user_prompt, raw=raw, on_event=on_event)
226
+
227
+ def run_prompt(
228
+ self, user_prompt: str, raw: bool = False, on_event: Optional[Callable] = None
229
+ ) -> None:
230
+ """
231
+ Handles a single prompt, using the blocking event-driven chat interface.
232
+ Optionally takes an on_event callback for custom event handling.
233
+ """
234
+ try:
235
+ self._print_verbose_debug("Calling agent.chat()...")
236
+
237
+ # Show waiting status with elapsed time
238
+ start_time = time.time()
239
+
240
+ # Get provider and model info for status display
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
+ )
255
+ # Thread coordination event
256
+ stop_updater = threading.Event()
257
+
258
+ def update_status():
259
+ elapsed = time.time() - start_time
260
+ status.update(
261
+ f"[bold blue]Waiting for {provider_name} (model: {model_name})... ({elapsed:.1f}s)[/bold blue]"
262
+ )
263
+
264
+ # Start status display and update timer
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()
290
+ if hasattr(self.agent, "set_latest_event"):
291
+ self.agent.set_latest_event(final_event)
292
+ self.agent.last_event = final_event
293
+ self._print_verbose_debug(f"agent.chat() returned: {final_event}")
294
+ self._print_verbose_final_event(final_event)
295
+ if on_event and final_event is not None:
296
+ on_event(final_event)
297
+ global_event_bus.publish(final_event)
298
+ except KeyboardInterrupt:
299
+ # Capture user interrupt / cancellation
300
+ self.console.print("[red]Interrupted by the user.[/red]")
301
+ try:
302
+ from janito.driver_events import RequestFinished, RequestStatus
303
+
304
+ # Record a synthetic "cancelled" final event so that downstream
305
+ # handlers (e.g. single_shot_mode.handler._post_prompt_actions)
306
+ # can reliably detect that the prompt was interrupted by the
307
+ # user and avoid showing misleading messages such as
308
+ # "No output produced by the model.".
309
+ if hasattr(self, "agent") and self.agent is not None:
310
+ self.agent.last_event = RequestFinished(
311
+ status=RequestStatus.CANCELLED,
312
+ reason="Interrupted by the user",
313
+ )
314
+ except Exception:
315
+ # Do not fail on cleanup – this hook is best-effort only.
316
+ pass
317
+
318
+ def _print_verbose_debug(self, message):
319
+ if hasattr(self.args, "verbose_agent") and self.args.verbose_agent:
320
+ print(f"[prompt_core][DEBUG] {message}")
321
+
322
+ def _print_verbose_final_event(self, final_event):
323
+ if hasattr(self.args, "verbose_agent") and self.args.verbose_agent:
324
+ print("[prompt_core][DEBUG] Received final_event from agent.chat:")
325
+ print(f" [prompt_core][DEBUG] type={type(final_event)}")
326
+ print(f" [prompt_core][DEBUG] content={final_event}")
327
+
328
+ def run_prompts(
329
+ self, prompts: list, raw: bool = False, on_event: Optional[Callable] = None
330
+ ) -> None:
331
+ """
332
+ Handles multiple prompts in sequence, collecting performance data for each.
333
+ """
334
+ for prompt in prompts:
335
+ self.run_prompt(prompt, raw=raw, on_event=on_event)
336
+ # No return value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: janito
3
- Version: 3.10.0
3
+ Version: 3.12.0
4
4
  Summary: A new Python package called janito.
5
5
  Author-email: João Pinto <janito@ikignosis.org>
6
6
  Project-URL: Homepage, https://github.com/ikignosis/janito
@@ -32,7 +32,7 @@ janito/cli/config.py,sha256=HkZ14701HzIqrvaNyDcDhGlVHfpX_uHlLp2rHmhRm_k,872
32
32
  janito/cli/console.py,sha256=gJolqzWL7jEPLxeuH-CwBDRFpXt976KdZOEAB2tdBDs,64
33
33
  janito/cli/main.py,sha256=s5odou0txf8pzTf1ADk2yV7T5m8B6cejJ81e7iu776U,312
34
34
  janito/cli/main_cli.py,sha256=0_mB-FKF-5ilUVZj3xL_MTmiZ-0NGmT2jDZYceiS6xE,16920
35
- janito/cli/prompt_core.py,sha256=EIqvWuYh-stVDL6Jcbg3YAlT2wfVi70N07iGo0bgrts,13504
35
+ janito/cli/prompt_core.py,sha256=x1RcX1p4wCL4YzXS76T6BZ8nt5cGiXCAB3QOlC6BJQQ,14937
36
36
  janito/cli/prompt_handler.py,sha256=SnPTlL64noeAMGlI08VBDD5IDD8jlVMIYA4-fS8zVLg,215
37
37
  janito/cli/prompt_setup.py,sha256=s48gvNfZhKjsEhf4EzL1tKIGm4wDidPMDvlM6TAPYes,2116
38
38
  janito/cli/rich_terminal_reporter.py,sha256=Lhfsicxvqjz1eWh51lGU2huCNJXfD6sCELZMReTbzF0,6659
@@ -263,9 +263,9 @@ janito/tools/tool_utils.py,sha256=alPm9DvtXSw_zPRKvP5GjbebPRf_nfvmWk2TNlL5Cws,12
263
263
  janito/tools/tools_adapter.py,sha256=Vd7A2tJ1q-EpoenkztK4he-0WhGjLFDP0vEfmXdFiNk,21361
264
264
  janito/tools/tools_schema.py,sha256=rGrKrmpPNR07VXHAJ_haGBRRO-YGLOF51BlYRep9AAQ,4415
265
265
  janito/tools/url_whitelist.py,sha256=0CPLkHTp5HgnwgjxwgXnJmwPeZQ30q4j3YjW59hiUUE,4295
266
- janito-3.10.0.dist-info/licenses/LICENSE,sha256=dXV4fOF2ZErugtN8l_Nrj5tsRTYgtjE3cgiya0UfBio,11356
267
- janito-3.10.0.dist-info/METADATA,sha256=x2KO5oXZMGPqy6ooQbJRkac0DQWGj17lBCBn4WLhYoU,2255
268
- janito-3.10.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
269
- janito-3.10.0.dist-info/entry_points.txt,sha256=wIo5zZxbmu4fC-ZMrsKD0T0vq7IqkOOLYhrqRGypkx4,48
270
- janito-3.10.0.dist-info/top_level.txt,sha256=m0NaVCq0-ivxbazE2-ND0EA9Hmuijj_OGkmCbnBcCig,7
271
- janito-3.10.0.dist-info/RECORD,,
266
+ janito-3.12.0.dist-info/licenses/LICENSE,sha256=dXV4fOF2ZErugtN8l_Nrj5tsRTYgtjE3cgiya0UfBio,11356
267
+ janito-3.12.0.dist-info/METADATA,sha256=oLtz2kTnIo6n65jlaaf1hsUt8Wnj8F3V-jZKcgdQYKE,2255
268
+ janito-3.12.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
269
+ janito-3.12.0.dist-info/entry_points.txt,sha256=wIo5zZxbmu4fC-ZMrsKD0T0vq7IqkOOLYhrqRGypkx4,48
270
+ janito-3.12.0.dist-info/top_level.txt,sha256=m0NaVCq0-ivxbazE2-ND0EA9Hmuijj_OGkmCbnBcCig,7
271
+ janito-3.12.0.dist-info/RECORD,,