janito 1.14.2__py3-none-any.whl → 2.0.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.
Files changed (282) hide show
  1. janito/__init__.py +6 -1
  2. janito/__main__.py +1 -1
  3. janito/agent/setup_agent.py +139 -0
  4. janito/agent/templates/profiles/{system_prompt_template_base.txt.j2 → system_prompt_template_main.txt.j2} +1 -1
  5. janito/cli/__init__.py +9 -0
  6. janito/cli/chat_mode/bindings.py +37 -0
  7. janito/cli/chat_mode/chat_entry.py +23 -0
  8. janito/cli/chat_mode/prompt_style.py +19 -0
  9. janito/cli/chat_mode/session.py +272 -0
  10. janito/{shell/prompt/completer.py → cli/chat_mode/shell/autocomplete.py} +7 -6
  11. janito/cli/chat_mode/shell/commands/__init__.py +55 -0
  12. janito/cli/chat_mode/shell/commands/base.py +9 -0
  13. janito/cli/chat_mode/shell/commands/clear.py +12 -0
  14. janito/{shell → cli/chat_mode/shell}/commands/conversation_restart.py +34 -30
  15. janito/cli/chat_mode/shell/commands/edit.py +25 -0
  16. janito/cli/chat_mode/shell/commands/help.py +16 -0
  17. janito/cli/chat_mode/shell/commands/history_view.py +93 -0
  18. janito/cli/chat_mode/shell/commands/lang.py +25 -0
  19. janito/cli/chat_mode/shell/commands/last.py +137 -0
  20. janito/cli/chat_mode/shell/commands/livelogs.py +49 -0
  21. janito/cli/chat_mode/shell/commands/multi.py +51 -0
  22. janito/cli/chat_mode/shell/commands/prompt.py +64 -0
  23. janito/cli/chat_mode/shell/commands/role.py +36 -0
  24. janito/cli/chat_mode/shell/commands/session.py +40 -0
  25. janito/{shell → cli/chat_mode/shell}/commands/session_control.py +2 -2
  26. janito/cli/chat_mode/shell/commands/termweb_log.py +92 -0
  27. janito/cli/chat_mode/shell/commands/tools.py +32 -0
  28. janito/{shell → cli/chat_mode/shell}/commands/utility.py +4 -7
  29. janito/{shell → cli/chat_mode/shell}/commands/verbose.py +5 -5
  30. janito/cli/chat_mode/shell/session/__init__.py +1 -0
  31. janito/{shell → cli/chat_mode/shell}/session/manager.py +9 -1
  32. janito/cli/chat_mode/toolbar.py +90 -0
  33. janito/cli/cli_commands/list_models.py +35 -0
  34. janito/cli/cli_commands/list_providers.py +9 -0
  35. janito/cli/cli_commands/list_tools.py +53 -0
  36. janito/cli/cli_commands/model_selection.py +50 -0
  37. janito/cli/cli_commands/model_utils.py +84 -0
  38. janito/cli/cli_commands/set_api_key.py +19 -0
  39. janito/cli/cli_commands/show_config.py +51 -0
  40. janito/cli/cli_commands/show_system_prompt.py +62 -0
  41. janito/cli/config.py +28 -0
  42. janito/cli/console.py +3 -0
  43. janito/cli/core/__init__.py +4 -0
  44. janito/cli/core/event_logger.py +59 -0
  45. janito/cli/core/getters.py +31 -0
  46. janito/cli/core/runner.py +141 -0
  47. janito/cli/core/setters.py +174 -0
  48. janito/cli/core/unsetters.py +54 -0
  49. janito/cli/main.py +8 -196
  50. janito/cli/main_cli.py +312 -0
  51. janito/cli/prompt_core.py +230 -0
  52. janito/cli/prompt_handler.py +6 -0
  53. janito/cli/rich_terminal_reporter.py +101 -0
  54. janito/cli/single_shot_mode/__init__.py +6 -0
  55. janito/cli/single_shot_mode/handler.py +137 -0
  56. janito/cli/termweb_starter.py +73 -24
  57. janito/cli/utils.py +25 -0
  58. janito/cli/verbose_output.py +196 -0
  59. janito/config.py +5 -0
  60. janito/config_manager.py +110 -0
  61. janito/conversation_history.py +30 -0
  62. janito/{agent/tools_utils/dir_walk_utils.py → dir_walk_utils.py} +3 -2
  63. janito/driver_events.py +98 -0
  64. janito/drivers/anthropic/driver.py +113 -0
  65. janito/drivers/azure_openai/driver.py +36 -0
  66. janito/drivers/driver_registry.py +33 -0
  67. janito/drivers/google_genai/driver.py +54 -0
  68. janito/drivers/google_genai/schema_generator.py +67 -0
  69. janito/drivers/mistralai/driver.py +41 -0
  70. janito/drivers/openai/driver.py +334 -0
  71. janito/event_bus/__init__.py +2 -0
  72. janito/event_bus/bus.py +68 -0
  73. janito/event_bus/event.py +15 -0
  74. janito/event_bus/handler.py +31 -0
  75. janito/event_bus/queue_bus.py +57 -0
  76. janito/exceptions.py +23 -0
  77. janito/formatting_token.py +54 -0
  78. janito/i18n/pt.py +1 -0
  79. janito/llm/__init__.py +5 -0
  80. janito/llm/agent.py +443 -0
  81. janito/llm/auth.py +62 -0
  82. janito/llm/driver.py +239 -0
  83. janito/llm/driver_config.py +34 -0
  84. janito/llm/driver_config_builder.py +34 -0
  85. janito/llm/driver_input.py +12 -0
  86. janito/llm/message_parts.py +60 -0
  87. janito/llm/model.py +38 -0
  88. janito/llm/provider.py +187 -0
  89. janito/perf_singleton.py +3 -0
  90. janito/performance_collector.py +167 -0
  91. janito/provider_config.py +98 -0
  92. janito/provider_registry.py +152 -0
  93. janito/providers/__init__.py +7 -0
  94. janito/providers/anthropic/model_info.py +22 -0
  95. janito/providers/anthropic/provider.py +65 -0
  96. janito/providers/azure_openai/model_info.py +15 -0
  97. janito/providers/azure_openai/provider.py +72 -0
  98. janito/providers/deepseek/__init__.py +1 -0
  99. janito/providers/deepseek/model_info.py +16 -0
  100. janito/providers/deepseek/provider.py +91 -0
  101. janito/providers/google/__init__.py +1 -0
  102. janito/providers/google/model_info.py +40 -0
  103. janito/providers/google/provider.py +69 -0
  104. janito/providers/mistralai/model_info.py +37 -0
  105. janito/providers/mistralai/provider.py +69 -0
  106. janito/providers/openai/__init__.py +1 -0
  107. janito/providers/openai/model_info.py +137 -0
  108. janito/providers/openai/provider.py +107 -0
  109. janito/providers/openai/schema_generator.py +63 -0
  110. janito/providers/provider_static_info.py +21 -0
  111. janito/providers/registry.py +26 -0
  112. janito/report_events.py +38 -0
  113. janito/termweb/app.py +1 -1
  114. janito/tools/__init__.py +16 -0
  115. janito/tools/adapters/__init__.py +1 -0
  116. janito/tools/adapters/local/__init__.py +54 -0
  117. janito/tools/adapters/local/adapter.py +92 -0
  118. janito/{agent/tools → tools/adapters/local}/ask_user.py +30 -13
  119. janito/tools/adapters/local/copy_file.py +84 -0
  120. janito/{agent/tools → tools/adapters/local}/create_directory.py +11 -10
  121. janito/tools/adapters/local/create_file.py +82 -0
  122. janito/tools/adapters/local/delete_text_in_file.py +136 -0
  123. janito/{agent/tools → tools/adapters/local}/fetch_url.py +18 -19
  124. janito/tools/adapters/local/find_files.py +140 -0
  125. janito/tools/adapters/local/get_file_outline/core.py +151 -0
  126. janito/{agent/tools → tools/adapters/local}/get_file_outline/python_outline.py +125 -0
  127. janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -0
  128. janito/{agent/tools → tools/adapters/local}/get_file_outline/search_outline.py +12 -7
  129. janito/{agent/tools → tools/adapters/local}/move_file.py +13 -9
  130. janito/{agent/tools → tools/adapters/local}/open_url.py +7 -5
  131. janito/tools/adapters/local/python_code_run.py +165 -0
  132. janito/tools/adapters/local/python_command_run.py +163 -0
  133. janito/tools/adapters/local/python_file_run.py +162 -0
  134. janito/{agent/tools → tools/adapters/local}/remove_directory.py +15 -9
  135. janito/{agent/tools → tools/adapters/local}/remove_file.py +17 -14
  136. janito/{agent/tools → tools/adapters/local}/replace_text_in_file.py +27 -22
  137. janito/tools/adapters/local/run_bash_command.py +176 -0
  138. janito/tools/adapters/local/run_powershell_command.py +219 -0
  139. janito/{agent/tools → tools/adapters/local}/search_text/core.py +32 -12
  140. janito/{agent/tools → tools/adapters/local}/search_text/match_lines.py +13 -4
  141. janito/{agent/tools → tools/adapters/local}/search_text/pattern_utils.py +12 -4
  142. janito/{agent/tools → tools/adapters/local}/search_text/traverse_directory.py +15 -2
  143. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/core.py +12 -11
  144. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/css_validator.py +1 -1
  145. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/html_validator.py +1 -1
  146. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/js_validator.py +1 -1
  147. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/json_validator.py +1 -1
  148. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/markdown_validator.py +1 -1
  149. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/ps1_validator.py +1 -1
  150. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/python_validator.py +1 -1
  151. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/xml_validator.py +1 -1
  152. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/yaml_validator.py +1 -1
  153. janito/{agent/tools/get_lines.py → tools/adapters/local/view_file.py} +45 -27
  154. janito/tools/inspect_registry.py +17 -0
  155. janito/tools/tool_base.py +105 -0
  156. janito/tools/tool_events.py +58 -0
  157. janito/tools/tool_run_exception.py +12 -0
  158. janito/{agent → tools}/tool_use_tracker.py +2 -4
  159. janito/{agent/tools_utils/utils.py → tools/tool_utils.py} +18 -9
  160. janito/tools/tools_adapter.py +207 -0
  161. janito/tools/tools_schema.py +104 -0
  162. janito/utils.py +11 -0
  163. janito/version.py +4 -0
  164. janito-2.0.0.dist-info/METADATA +232 -0
  165. janito-2.0.0.dist-info/RECORD +180 -0
  166. janito/agent/__init__.py +0 -0
  167. janito/agent/api_exceptions.py +0 -4
  168. janito/agent/config.py +0 -147
  169. janito/agent/config_defaults.py +0 -12
  170. janito/agent/config_utils.py +0 -0
  171. janito/agent/content_handler.py +0 -0
  172. janito/agent/conversation.py +0 -238
  173. janito/agent/conversation_api.py +0 -306
  174. janito/agent/conversation_exceptions.py +0 -18
  175. janito/agent/conversation_tool_calls.py +0 -39
  176. janito/agent/conversation_ui.py +0 -17
  177. janito/agent/event.py +0 -24
  178. janito/agent/event_dispatcher.py +0 -24
  179. janito/agent/event_handler_protocol.py +0 -5
  180. janito/agent/event_system.py +0 -15
  181. janito/agent/llm_conversation_history.py +0 -82
  182. janito/agent/message_handler.py +0 -20
  183. janito/agent/message_handler_protocol.py +0 -5
  184. janito/agent/openai_client.py +0 -149
  185. janito/agent/openai_schema_generator.py +0 -187
  186. janito/agent/profile_manager.py +0 -96
  187. janito/agent/queued_message_handler.py +0 -50
  188. janito/agent/rich_live.py +0 -32
  189. janito/agent/rich_message_handler.py +0 -115
  190. janito/agent/runtime_config.py +0 -36
  191. janito/agent/test_handler_protocols.py +0 -47
  192. janito/agent/test_openai_schema_generator.py +0 -93
  193. janito/agent/tests/__init__.py +0 -1
  194. janito/agent/tool_base.py +0 -63
  195. janito/agent/tool_executor.py +0 -122
  196. janito/agent/tool_registry.py +0 -49
  197. janito/agent/tools/__init__.py +0 -47
  198. janito/agent/tools/create_file.py +0 -59
  199. janito/agent/tools/delete_text_in_file.py +0 -97
  200. janito/agent/tools/find_files.py +0 -106
  201. janito/agent/tools/get_file_outline/core.py +0 -81
  202. janito/agent/tools/present_choices.py +0 -64
  203. janito/agent/tools/python_command_runner.py +0 -201
  204. janito/agent/tools/python_file_runner.py +0 -199
  205. janito/agent/tools/python_stdin_runner.py +0 -208
  206. janito/agent/tools/replace_file.py +0 -72
  207. janito/agent/tools/run_bash_command.py +0 -218
  208. janito/agent/tools/run_powershell_command.py +0 -251
  209. janito/agent/tools_utils/__init__.py +0 -1
  210. janito/agent/tools_utils/action_type.py +0 -7
  211. janito/agent/tools_utils/test_gitignore_utils.py +0 -46
  212. janito/cli/_livereload_log_utils.py +0 -13
  213. janito/cli/_print_config.py +0 -96
  214. janito/cli/_termweb_log_utils.py +0 -17
  215. janito/cli/_utils.py +0 -9
  216. janito/cli/arg_parser.py +0 -272
  217. janito/cli/cli_main.py +0 -281
  218. janito/cli/config_commands.py +0 -211
  219. janito/cli/config_runner.py +0 -35
  220. janito/cli/formatting_runner.py +0 -12
  221. janito/cli/livereload_starter.py +0 -60
  222. janito/cli/logging_setup.py +0 -38
  223. janito/cli/one_shot.py +0 -80
  224. janito/livereload/app.py +0 -25
  225. janito/rich_utils.py +0 -59
  226. janito/shell/__init__.py +0 -0
  227. janito/shell/commands/__init__.py +0 -61
  228. janito/shell/commands/config.py +0 -22
  229. janito/shell/commands/edit.py +0 -24
  230. janito/shell/commands/history_view.py +0 -18
  231. janito/shell/commands/lang.py +0 -19
  232. janito/shell/commands/livelogs.py +0 -42
  233. janito/shell/commands/prompt.py +0 -62
  234. janito/shell/commands/termweb_log.py +0 -94
  235. janito/shell/commands/tools.py +0 -26
  236. janito/shell/commands/track.py +0 -36
  237. janito/shell/main.py +0 -326
  238. janito/shell/prompt/load_prompt.py +0 -57
  239. janito/shell/prompt/session_setup.py +0 -57
  240. janito/shell/session/config.py +0 -109
  241. janito/shell/session/history.py +0 -0
  242. janito/shell/ui/interactive.py +0 -226
  243. janito/termweb/static/editor.css +0 -158
  244. janito/termweb/static/editor.css.bak +0 -145
  245. janito/termweb/static/editor.html +0 -46
  246. janito/termweb/static/editor.html.bak +0 -46
  247. janito/termweb/static/editor.js +0 -265
  248. janito/termweb/static/editor.js.bak +0 -259
  249. janito/termweb/static/explorer.html.bak +0 -59
  250. janito/termweb/static/favicon.ico +0 -0
  251. janito/termweb/static/favicon.ico.bak +0 -0
  252. janito/termweb/static/index.html +0 -53
  253. janito/termweb/static/index.html.bak +0 -54
  254. janito/termweb/static/index.html.bak.bak +0 -175
  255. janito/termweb/static/landing.html.bak +0 -36
  256. janito/termweb/static/termicon.svg +0 -1
  257. janito/termweb/static/termweb.css +0 -214
  258. janito/termweb/static/termweb.css.bak +0 -237
  259. janito/termweb/static/termweb.js +0 -162
  260. janito/termweb/static/termweb.js.bak +0 -168
  261. janito/termweb/static/termweb.js.bak.bak +0 -157
  262. janito/termweb/static/termweb_quickopen.js +0 -135
  263. janito/termweb/static/termweb_quickopen.js.bak +0 -125
  264. janito/tests/test_rich_utils.py +0 -44
  265. janito/web/__init__.py +0 -0
  266. janito/web/__main__.py +0 -25
  267. janito/web/app.py +0 -145
  268. janito-1.14.2.dist-info/METADATA +0 -306
  269. janito-1.14.2.dist-info/RECORD +0 -162
  270. janito-1.14.2.dist-info/licenses/LICENSE +0 -21
  271. /janito/{shell → cli/chat_mode/shell}/input_history.py +0 -0
  272. /janito/{shell/commands/session.py → cli/chat_mode/shell/session/history.py} +0 -0
  273. /janito/{agent/tools_utils/formatting.py → formatting.py} +0 -0
  274. /janito/{agent/tools_utils/gitignore_utils.py → gitignore_utils.py} +0 -0
  275. /janito/{agent/platform_discovery.py → platform_discovery.py} +0 -0
  276. /janito/{agent/tools → tools/adapters/local}/get_file_outline/__init__.py +0 -0
  277. /janito/{agent/tools → tools/adapters/local}/get_file_outline/markdown_outline.py +0 -0
  278. /janito/{agent/tools → tools/adapters/local}/search_text/__init__.py +0 -0
  279. /janito/{agent/tools → tools/adapters/local}/validate_file_syntax/__init__.py +0 -0
  280. {janito-1.14.2.dist-info → janito-2.0.0.dist-info}/WHEEL +0 -0
  281. {janito-1.14.2.dist-info → janito-2.0.0.dist-info}/entry_points.txt +0 -0
  282. {janito-1.14.2.dist-info → janito-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,230 @@
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.version 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 RequestStarted, RequestFinished, RequestStatus
12
+ from janito.tools.tool_events import ToolCallError
13
+ import threading
14
+ from janito.cli.verbose_output import print_verbose_header
15
+ from janito.event_bus import event_bus as global_event_bus
16
+
17
+
18
+ class StatusRef:
19
+ def __init__(self):
20
+ self.status = None
21
+
22
+
23
+ class PromptHandler:
24
+ args: Any
25
+ agent: Any
26
+ performance_collector: PerformanceCollector
27
+ console: Console
28
+ provider_instance: Any
29
+
30
+ def __init__(self, args: Any, conversation_history, provider_instance) -> None:
31
+ self.temperature = args.temperature if hasattr(args, "temperature") else None
32
+ """
33
+ Initialize PromptHandler.
34
+ :param args: CLI or programmatic arguments for provider/model selection, etc.
35
+ :param conversation_history: LLMConversationHistory object for multi-turn chat mode.
36
+ :param provider_instance: An initialized provider instance.
37
+ """
38
+ self.args = args
39
+ self.conversation_history = conversation_history
40
+ self.provider_instance = provider_instance
41
+ self.agent = None
42
+ from janito.perf_singleton import performance_collector
43
+
44
+ self.performance_collector = performance_collector
45
+ self.console = Console()
46
+
47
+ def _handle_inner_event(self, inner_event, on_event, status):
48
+ if on_event:
49
+ on_event(inner_event)
50
+ from janito.tools.tool_events import ToolCallFinished
51
+
52
+ # Print tool result if ToolCallFinished event is received
53
+ if isinstance(inner_event, ToolCallFinished):
54
+ # Print result if verbose_tools is enabled or always for user visibility
55
+ if hasattr(self.args, "verbose_tools") and self.args.verbose_tools:
56
+ self.console.print(
57
+ f"[cyan][tools-adapter] Tool '{inner_event.tool_name}' result:[/cyan] {inner_event.result}"
58
+ )
59
+ else:
60
+ self.console.print(inner_event.result)
61
+ return None
62
+ if isinstance(inner_event, RequestFinished):
63
+ status.update("[bold green]Received response![bold green]")
64
+ return "break"
65
+ elif (
66
+ isinstance(inner_event, RequestFinished)
67
+ and getattr(inner_event, "status", None) == "error"
68
+ ):
69
+ error_msg = (
70
+ inner_event.error if hasattr(inner_event, "error") else "Unknown error"
71
+ )
72
+ if (
73
+ "Status 429" in error_msg
74
+ and "Service tier capacity exceeded for this model" in error_msg
75
+ ):
76
+ status.update(
77
+ "[yellow]Service tier capacity exceeded, retrying...[yellow]"
78
+ )
79
+ return "break"
80
+ status.update(f"[bold red]Error: {error_msg}[bold red]")
81
+ self.console.print(f"[red]Error: {error_msg}[red]")
82
+ return "break"
83
+ elif isinstance(inner_event, ToolCallError):
84
+ error_msg = (
85
+ inner_event.error
86
+ if hasattr(inner_event, "error")
87
+ else "Unknown tool error"
88
+ )
89
+ tool_name = (
90
+ inner_event.tool_name
91
+ if hasattr(inner_event, "tool_name")
92
+ else "unknown"
93
+ )
94
+ status.update(
95
+ f"[bold red]Tool Error in '{tool_name}': {error_msg}[bold red]"
96
+ )
97
+ self.console.print(f"[red]Tool Error in '{tool_name}': {error_msg}[red]")
98
+ return "break"
99
+ elif isinstance(inner_event, RequestFinished) and getattr(
100
+ inner_event, "status", None
101
+ ) in (RequestStatus.EMPTY_RESPONSE, RequestStatus.TIMEOUT):
102
+ details = getattr(inner_event, "details", None) or {}
103
+ block_reason = details.get("block_reason")
104
+ block_msg = details.get("block_reason_message")
105
+ msg = details.get(
106
+ "message", "LLM returned an empty or incomplete response."
107
+ )
108
+ driver_name = getattr(inner_event, "driver_name", "unknown driver")
109
+ if block_reason or block_msg:
110
+ status.update(
111
+ f"[bold yellow]Blocked by driver: {driver_name} | {block_reason or ''} {block_msg or ''}[bold yellow]"
112
+ )
113
+ self.console.print(
114
+ f"[yellow]Blocked by driver: {driver_name} (empty response): {block_reason or ''}\n{block_msg or ''}[/yellow]"
115
+ )
116
+ else:
117
+ status.update(
118
+ f"[yellow]LLM produced no output for this request (driver: {driver_name}).[/yellow]"
119
+ )
120
+ self.console.print(
121
+ f"[yellow]Warning: {msg} (driver: {driver_name})[/yellow]"
122
+ )
123
+ return "break"
124
+ # Report unknown event types
125
+ event_type = type(inner_event).__name__
126
+ self.console.print(
127
+ f"[yellow]Warning: Unknown event type encountered: {event_type}[yellow]"
128
+ )
129
+ return None
130
+
131
+ def _process_event_iter(self, event_iter, on_event):
132
+ for event in event_iter:
133
+ # Handle exceptions from generation thread
134
+ if isinstance(event, dict) and event.get("type") == "exception":
135
+ self.console.print("[red]Exception in generation thread:[red]")
136
+ self.console.print(event.get("traceback", "No traceback available"))
137
+ break
138
+ if on_event:
139
+ on_event(event)
140
+ if isinstance(event, RequestStarted):
141
+ pass # No change needed for started event
142
+ elif isinstance(event, RequestFinished) and getattr(
143
+ event, "status", None
144
+ ) in ("error", "cancelled"):
145
+ # Handle error/cancelled as needed
146
+ for inner_event in event_iter:
147
+ result = self._handle_inner_event(inner_event, on_event, None)
148
+ if result == "break":
149
+ break
150
+ # After exiting, continue with next events (if any)
151
+ # Handle other event types outside the spinner if needed
152
+ elif isinstance(event, RequestFinished) and getattr(
153
+ event, "status", None
154
+ ) in (RequestStatus.EMPTY_RESPONSE, RequestStatus.TIMEOUT):
155
+ details = getattr(event, "details", None) or {}
156
+ block_reason = details.get("block_reason")
157
+ block_msg = details.get("block_reason_message")
158
+ msg = details.get(
159
+ "message", "LLM returned an empty or incomplete response."
160
+ )
161
+ driver_name = getattr(event, "driver_name", "unknown driver")
162
+ if block_reason or block_msg:
163
+ self.console.print(
164
+ f"[yellow]Blocked by driver: {driver_name} (empty response): {block_reason or ''}\n{block_msg or ''}[/yellow]"
165
+ )
166
+ else:
167
+ self.console.print(
168
+ f"[yellow]Warning: {msg} (driver: {driver_name})[/yellow]"
169
+ )
170
+ else:
171
+ pass
172
+
173
+ def handle_prompt(
174
+ self, user_prompt, args=None, print_header=True, raw=False, on_event=None
175
+ ):
176
+ # args defaults to self.args for compatibility in interactive mode
177
+ args = (
178
+ args if args is not None else self.args if hasattr(self, "args") else None
179
+ )
180
+ # Join/cleanup prompt
181
+ if isinstance(user_prompt, list):
182
+ user_prompt = " ".join(user_prompt).strip()
183
+ else:
184
+ user_prompt = str(user_prompt).strip() if user_prompt is not None else ""
185
+ if not user_prompt:
186
+ raise ValueError("No user prompt was provided!")
187
+ if print_header and hasattr(self, "agent") and args is not None:
188
+ print_verbose_header(self.agent, args)
189
+ self.run_prompt(user_prompt, raw=raw, on_event=on_event)
190
+
191
+ def run_prompt(
192
+ self, user_prompt: str, raw: bool = False, on_event: Optional[Callable] = None
193
+ ) -> None:
194
+ """
195
+ Handles a single prompt, using the blocking event-driven chat interface.
196
+ Optionally takes an on_event callback for custom event handling.
197
+ """
198
+ try:
199
+ self._print_verbose_debug("Calling agent.chat()...")
200
+ final_event = self.agent.chat(prompt=user_prompt)
201
+ if hasattr(self.agent, "set_latest_event"):
202
+ self.agent.set_latest_event(final_event)
203
+ self.agent.last_event = final_event
204
+ self._print_verbose_debug(f"agent.chat() returned: {final_event}")
205
+ self._print_verbose_final_event(final_event)
206
+ if on_event and final_event is not None:
207
+ on_event(final_event)
208
+ global_event_bus.publish(final_event)
209
+ except KeyboardInterrupt:
210
+ self.console.print("[red]Request interrupted.[red]")
211
+
212
+ def _print_verbose_debug(self, message):
213
+ if hasattr(self.args, "verbose_agent") and self.args.verbose_agent:
214
+ print(f"[prompt_core][DEBUG] {message}")
215
+
216
+ def _print_verbose_final_event(self, final_event):
217
+ if hasattr(self.args, "verbose_agent") and self.args.verbose_agent:
218
+ print("[prompt_core][DEBUG] Received final_event from agent.chat:")
219
+ print(f" [prompt_core][DEBUG] type={type(final_event)}")
220
+ print(f" [prompt_core][DEBUG] content={final_event}")
221
+
222
+ def run_prompts(
223
+ self, prompts: list, raw: bool = False, on_event: Optional[Callable] = None
224
+ ) -> None:
225
+ """
226
+ Handles multiple prompts in sequence, collecting performance data for each.
227
+ """
228
+ for prompt in prompts:
229
+ self.run_prompt(prompt, raw=raw, on_event=on_event)
230
+ # No return value
@@ -0,0 +1,6 @@
1
+ """
2
+ Generic PromptHandler: Handles prompt submission and response formatting for janito CLI.
3
+ Supports both one-shot and multi-prompt scenarios.
4
+ """
5
+
6
+ from janito.cli.prompt_core import PromptHandler, StatusRef
@@ -0,0 +1,101 @@
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
+ class RichTerminalReporter(EventHandlerBase):
14
+ """
15
+ Handles UI rendering for janito events using Rich.
16
+
17
+ - For ResponseReceived events, iterates over the 'parts' field and displays each part appropriately:
18
+ - TextMessagePart: rendered as Markdown (uses 'content' field)
19
+ - Other MessageParts: displayed using Pretty or a suitable Rich representation
20
+ - For RequestFinished events, output is printed only if raw mode is enabled (using Pretty formatting).
21
+ - Report events (info, success, error, etc.) are always printed with appropriate styling.
22
+ """
23
+
24
+ def __init__(self, raw_mode=False):
25
+ from janito.cli.console import shared_console
26
+
27
+ self.console = shared_console
28
+ self.raw_mode = raw_mode
29
+ import janito.report_events as report_events
30
+
31
+ super().__init__(driver_events, report_events)
32
+ self._waiting_printed = False
33
+
34
+ def on_RequestStarted(self, event):
35
+ # Print waiting message with provider name
36
+ provider = None
37
+ if hasattr(event, "payload") and isinstance(event.payload, dict):
38
+ provider = event.payload.get("provider_name")
39
+ if not provider:
40
+ provider = getattr(event, "provider_name", None)
41
+ if not provider:
42
+ provider = getattr(event, "driver_name", None)
43
+ if not provider:
44
+ provider = "LLM"
45
+ self.console.print(f"[bold cyan]Waiting for {provider}...[/bold cyan]", end="")
46
+
47
+ def on_ResponseReceived(self, event):
48
+ parts = event.parts if hasattr(event, "parts") else None
49
+ if not parts:
50
+ self.console.print("[No response parts to display]")
51
+ self.console.file.flush()
52
+ return
53
+ for part in parts:
54
+ if isinstance(part, message_parts.TextMessagePart):
55
+ self.console.print(Markdown(part.content))
56
+ self.console.file.flush()
57
+
58
+ def on_RequestFinished(self, event):
59
+ self.console.print("") # Print end of line after waiting message
60
+ self._waiting_printed = False
61
+ response = event.response if hasattr(event, "response") else None
62
+ if response is not None:
63
+ if self.raw_mode:
64
+ self.console.print(Pretty(response, expand_all=True))
65
+ self.console.file.flush()
66
+ # Check for 'code' and 'event' fields in the response
67
+ code = None
68
+ event_field = None
69
+ if isinstance(response, dict):
70
+ code = response.get("code")
71
+ event_field = response.get("event")
72
+ if event_field is not None:
73
+ self.console.print(f"[bold yellow]Event:[/] {event_field}")
74
+ self.console.file.flush()
75
+ # No output if not raw_mode or if response is None
76
+
77
+ def on_ReportEvent(self, event):
78
+ msg = event.message if hasattr(event, "message") else None
79
+ subtype = event.subtype if hasattr(event, "subtype") else None
80
+ if not msg or not subtype:
81
+ return
82
+ if subtype == ReportSubtype.ACTION_INFO:
83
+ if getattr(event, "action", None) in (
84
+ getattr(ReportAction, "UPDATE", None),
85
+ getattr(ReportAction, "WRITE", None),
86
+ getattr(ReportAction, "DELETE", None),
87
+ ):
88
+ self.console.print(f"[magenta]{msg}[/magenta]", end="")
89
+ else:
90
+ self.console.print(msg, end="")
91
+ self.console.file.flush()
92
+ elif subtype in (
93
+ ReportSubtype.SUCCESS,
94
+ ReportSubtype.ERROR,
95
+ ReportSubtype.WARNING,
96
+ ):
97
+ self.console.print(msg)
98
+ self.console.file.flush()
99
+ else:
100
+ self.console.print(msg)
101
+ self.console.file.flush()
@@ -0,0 +1,6 @@
1
+ # janito.cli.single_shot_mode package
2
+ from .handler import PromptHandler
3
+
4
+ __all__ = [
5
+ "PromptHandler",
6
+ ]
@@ -0,0 +1,137 @@
1
+ """
2
+ PromptHandler: Handles prompt submission and response formatting for janito CLI (one-shot prompt execution).
3
+ """
4
+
5
+ import time
6
+ from janito.version import __version__ as VERSION
7
+ from janito.cli.prompt_core import PromptHandler as GenericPromptHandler
8
+ from janito.cli.verbose_output import (
9
+ print_verbose_header,
10
+ print_performance,
11
+ handle_exception,
12
+ )
13
+ import janito.tools # Ensure all tools are registered
14
+ from janito.cli.console import shared_console
15
+
16
+
17
+ class PromptHandler:
18
+ def __init__(self, args, provider_instance, llm_driver_config, role=None):
19
+ self.args = args
20
+ self.provider_instance = provider_instance
21
+ self.llm_driver_config = llm_driver_config
22
+ self.role = role
23
+ from janito.agent.setup_agent import create_configured_agent
24
+
25
+ self.agent = create_configured_agent(
26
+ provider_instance=provider_instance,
27
+ llm_driver_config=llm_driver_config,
28
+ role=role,
29
+ verbose_tools=getattr(args, "verbose_tools", False),
30
+ verbose_agent=getattr(args, "verbose_agent", False),
31
+ )
32
+ # Setup conversation/history if needed
33
+ self.generic_handler = GenericPromptHandler(
34
+ args, [], provider_instance=provider_instance
35
+ )
36
+ self.generic_handler.agent = self.agent
37
+
38
+ def handle(self) -> None:
39
+ import traceback
40
+
41
+ user_prompt = " ".join(getattr(self.args, "user_prompt", [])).strip()
42
+ # UTF-8 sanitize user_prompt
43
+ sanitized = user_prompt
44
+ try:
45
+ sanitized.encode("utf-8")
46
+ except UnicodeEncodeError:
47
+ sanitized = sanitized.encode("utf-8", errors="replace").decode("utf-8")
48
+ shared_console.print(
49
+ "[yellow]Warning: Some characters in your input were not valid UTF-8 and have been replaced.[/yellow]"
50
+ )
51
+ try:
52
+ self.generic_handler.handle_prompt(
53
+ sanitized,
54
+ args=self.args,
55
+ print_header=True,
56
+ raw=getattr(self.args, "raw", False),
57
+ )
58
+ if hasattr(self.args, "verbose_agent") and self.args.verbose_agent:
59
+ print("[debug] handle_prompt() completed without exception.")
60
+ except Exception as e:
61
+ print(
62
+ f"[error] Exception occurred in handle_prompt: {type(e).__name__}: {e}"
63
+ )
64
+ traceback.print_exc()
65
+ self._post_prompt_actions()
66
+
67
+ def _post_prompt_actions(self):
68
+ final_event = getattr(self.agent, "last_event", None)
69
+ if final_event is not None:
70
+ self._print_exit_reason_and_parts(final_event)
71
+ # --- BEGIN: Print token info in rich rule if --verbose is set ---
72
+ if hasattr(self.args, "verbose") and self.args.verbose:
73
+ from janito.perf_singleton import performance_collector
74
+
75
+ token_info = performance_collector.get_last_request_usage()
76
+ from rich.rule import Rule
77
+ from rich import print as rich_print
78
+ from janito.cli.utils import format_tokens
79
+
80
+ if token_info:
81
+ if isinstance(token_info, dict):
82
+ token_str = " | ".join(
83
+ f"{k}: {format_tokens(v) if isinstance(v, int) else v}"
84
+ for k, v in token_info.items()
85
+ )
86
+ else:
87
+ token_str = str(token_info)
88
+ rich_print(Rule(f"[bold cyan]Token Usage[/bold cyan] {token_str}"))
89
+ else:
90
+ rich_print(Rule("[cyan]No token usage info available.[/cyan]"))
91
+ else:
92
+ shared_console.print("[yellow]No output produced by the model.[/yellow]")
93
+ self._cleanup_driver_and_console()
94
+
95
+ def _print_exit_reason_and_parts(self, final_event):
96
+ exit_reason = (
97
+ getattr(final_event, "metadata", {}).get("exit_reason")
98
+ if hasattr(final_event, "metadata")
99
+ else None
100
+ )
101
+ if exit_reason:
102
+ print(f"[bold yellow]Exit reason: {exit_reason}[/bold yellow]")
103
+ parts = getattr(final_event, "parts", None)
104
+ if not exit_reason:
105
+ if parts is None or len(parts) == 0:
106
+ shared_console.print(
107
+ "[yellow]No output produced by the model.[/yellow]"
108
+ )
109
+ else:
110
+ if hasattr(self.args, "verbose_agent") and self.args.verbose_agent:
111
+ print(
112
+ "[yellow]No user-visible output. Model returned the following parts:"
113
+ )
114
+ for idx, part in enumerate(parts):
115
+ print(
116
+ f" [part {idx}] type: {type(part).__name__} | content: {getattr(part, 'content', repr(part))}"
117
+ )
118
+
119
+ def _cleanup_driver_and_console(self):
120
+ if hasattr(self.agent, "join_driver"):
121
+ if (
122
+ hasattr(self.agent, "input_queue")
123
+ and self.agent.input_queue is not None
124
+ ):
125
+ self.agent.input_queue.put(None)
126
+ self.agent.join_driver()
127
+ try:
128
+ shared_console.file.flush()
129
+ except Exception:
130
+ pass
131
+ try:
132
+ import sys
133
+
134
+ sys.stdout.flush()
135
+ except Exception:
136
+ pass
137
+ # If event logger is active, flush event log
@@ -4,9 +4,11 @@ import tempfile
4
4
  import time
5
5
  import http.client
6
6
  import os
7
+ import threading
8
+ import queue
7
9
  from rich.console import Console
8
- from janito.cli._termweb_log_utils import print_termweb_logs
9
10
  from janito.i18n import tr
11
+ from janito.cli.config import get_termweb_port
10
12
 
11
13
 
12
14
  def wait_for_termweb(port, timeout=3.0):
@@ -25,49 +27,96 @@ def wait_for_termweb(port, timeout=3.0):
25
27
  return False
26
28
 
27
29
 
28
- def start_termweb(selected_port):
30
+ def termweb_start_and_watch(shell_state, shellstate_lock, selected_port=None):
29
31
  """
30
- Start the termweb server on the given port, with rich spinner and logging.
31
- Returns (termweb_proc, started: bool)
32
+ Start the termweb server on the given port in a background thread.
33
+ Communicates (termweb_proc, started: bool, stdout_path, stderr_path) via result_queue if provided.
34
+ Returns the Thread object.
32
35
  """
33
- console = Console()
34
- with console.status("[cyan]Starting web server...", spinner="dots"):
35
- # Step 1: Try source path
36
+
37
+ def termweb_worker(shell_state, shellstate_lock, selected_port, check_interval=2.0):
38
+ """
39
+ Worker to start termweb process, then monitor its running state/health, and update shell_state fields in a thread-safe manner.
40
+ - shell_state: context or state object to update (must provide .termweb_pid, .termweb_status, etc.)
41
+ - shellstate_lock: a threading.Lock or similar to synchronize access
42
+ """
43
+ console = Console()
44
+ # Try to locate app.py
36
45
  app_py_path = os.path.join(os.path.dirname(__file__), "..", "termweb", "app.py")
37
46
  app_py_path = os.path.abspath(app_py_path)
38
47
  if not os.path.isfile(app_py_path):
39
- # Step 2: Try installed package
40
48
  try:
41
49
  import janito_termweb
42
50
 
43
51
  app_py_path = janito_termweb.__file__.replace("__init__.py", "app.py")
44
52
  except ImportError:
45
- console.print("[red]Could not find termweb app.py![/red]")
46
- return None, False, None, None
53
+ with shellstate_lock:
54
+ shell_state.termweb_status = "notfound"
55
+ shell_state.termweb_pid = None
56
+ shell_state.termweb_stdout_path = None
57
+ shell_state.termweb_stderr_path = None
58
+ return
47
59
  termweb_stdout = tempfile.NamedTemporaryFile(
48
60
  delete=False, mode="w+", encoding="utf-8"
49
61
  )
50
62
  termweb_stderr = tempfile.NamedTemporaryFile(
51
63
  delete=False, mode="w+", encoding="utf-8"
52
64
  )
65
+ port_to_use = selected_port if selected_port is not None else get_termweb_port()
53
66
  termweb_proc = subprocess.Popen(
54
- [sys.executable, app_py_path, "--port", str(selected_port)],
67
+ [sys.executable, app_py_path, "--port", str(port_to_use)],
55
68
  stdout=termweb_stdout,
56
69
  stderr=termweb_stderr,
57
70
  )
58
- if wait_for_termweb(selected_port, timeout=3.0):
59
- console.print(
60
- tr(
61
- "TermWeb started... Available at http://localhost:{selected_port}",
62
- selected_port=selected_port,
63
- )
64
- )
65
- return termweb_proc, True, termweb_stdout.name, termweb_stderr.name
71
+
72
+ # Step 1: Wait for server to become healthy (initial check)
73
+ if wait_for_termweb(port_to_use, timeout=3.0):
74
+ with shellstate_lock:
75
+ shell_state.termweb_status = "running"
76
+ shell_state.termweb_pid = termweb_proc.pid
77
+ shell_state.termweb_stdout_path = termweb_stdout.name
78
+ shell_state.termweb_stderr_path = termweb_stderr.name
79
+ shell_state.termweb_port = port_to_use
66
80
  else:
67
81
  termweb_proc.terminate()
68
82
  termweb_proc.wait()
69
- console.print(
70
- f"[red]Failed to start TermWeb on port {selected_port}. Check logs for details.[/red]"
71
- )
72
- print_termweb_logs(termweb_stdout.name, termweb_stderr.name, console)
73
- return None, False, termweb_stdout.name, termweb_stderr.name
83
+ # console.print(f"[red]Failed to start TermWeb on port {port_to_use}. Check logs for details.[/red]")
84
+ with shellstate_lock:
85
+ shell_state.termweb_status = "failed-start"
86
+ shell_state.termweb_pid = None
87
+ shell_state.termweb_stdout_path = termweb_stdout.name
88
+ shell_state.termweb_stderr_path = termweb_stderr.name
89
+ shell_state.termweb_port = None
90
+ return
91
+
92
+ # Step 2: Run watcher loop; exit and set fields if process or health fails
93
+ import time
94
+ from http.client import HTTPConnection
95
+
96
+ while True:
97
+ if termweb_proc.poll() is not None: # means process exited
98
+ with shellstate_lock:
99
+ shell_state.termweb_status = "terminated"
100
+ shell_state.termweb_pid = None
101
+ break
102
+ try:
103
+ conn = HTTPConnection("localhost", port_to_use, timeout=0.5)
104
+ conn.request("GET", "/")
105
+ resp = conn.getresponse()
106
+ if resp.status != 200:
107
+ raise Exception("Bad status")
108
+ except Exception:
109
+ # console.print(f"[red]TermWeb on port {port_to_use} appears to have stopped responding![/red]")
110
+ with shellstate_lock:
111
+ shell_state.termweb_status = "unresponsive"
112
+ break
113
+ time.sleep(check_interval)
114
+
115
+ # Launch background thread
116
+ t = threading.Thread(
117
+ target=termweb_worker,
118
+ args=(shell_state, shellstate_lock, selected_port),
119
+ daemon=True,
120
+ )
121
+ t.start()
122
+ return t
janito/cli/utils.py ADDED
@@ -0,0 +1,25 @@
1
+ """
2
+ Utility functions for janito CLI (shared).
3
+ """
4
+
5
+
6
+ def format_tokens(n):
7
+ if n >= 1_000_000:
8
+ return f"{n/1_000_000:.2f}m"
9
+ elif n >= 1_000:
10
+ return f"{n/1_000:.2f}k"
11
+ else:
12
+ return str(n)
13
+
14
+
15
+ def format_generation_time(generation_time_ms):
16
+ minutes = int(generation_time_ms // 60000)
17
+ seconds = int((generation_time_ms % 60000) // 1000)
18
+ milliseconds = int(generation_time_ms % 1000)
19
+ formatted_time = ""
20
+ if minutes > 0:
21
+ formatted_time += f"{minutes}m "
22
+ if seconds > 0:
23
+ formatted_time += f"{seconds}s "
24
+ formatted_time += f"[{int(generation_time_ms)} ms]"
25
+ return formatted_time