janito 1.9.0__py3-none-any.whl → 1.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.
Files changed (106) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/api_exceptions.py +4 -0
  3. janito/agent/config.py +1 -1
  4. janito/agent/config_defaults.py +2 -26
  5. janito/agent/conversation.py +163 -122
  6. janito/agent/conversation_api.py +246 -168
  7. janito/agent/conversation_ui.py +1 -1
  8. janito/agent/{conversation_history.py → llm_conversation_history.py} +30 -1
  9. janito/agent/openai_client.py +38 -23
  10. janito/agent/openai_schema_generator.py +162 -129
  11. janito/agent/platform_discovery.py +134 -77
  12. janito/agent/profile_manager.py +5 -5
  13. janito/agent/rich_message_handler.py +80 -31
  14. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +20 -4
  15. janito/agent/test_openai_schema_generator.py +93 -0
  16. janito/agent/tool_base.py +7 -2
  17. janito/agent/tool_executor.py +54 -49
  18. janito/agent/tool_registry.py +5 -2
  19. janito/agent/tool_use_tracker.py +26 -5
  20. janito/agent/tools/__init__.py +8 -3
  21. janito/agent/tools/create_directory.py +3 -1
  22. janito/agent/tools/create_file.py +7 -1
  23. janito/agent/tools/fetch_url.py +40 -3
  24. janito/agent/tools/find_files.py +29 -14
  25. janito/agent/tools/get_file_outline/core.py +7 -8
  26. janito/agent/tools/get_file_outline/python_outline.py +139 -95
  27. janito/agent/tools/get_file_outline/search_outline.py +3 -1
  28. janito/agent/tools/get_lines.py +98 -64
  29. janito/agent/tools/move_file.py +59 -31
  30. janito/agent/tools/open_url.py +31 -0
  31. janito/agent/tools/present_choices.py +3 -1
  32. janito/agent/tools/python_command_runner.py +149 -0
  33. janito/agent/tools/python_file_runner.py +147 -0
  34. janito/agent/tools/python_stdin_runner.py +153 -0
  35. janito/agent/tools/remove_directory.py +3 -1
  36. janito/agent/tools/remove_file.py +5 -1
  37. janito/agent/tools/replace_file.py +12 -2
  38. janito/agent/tools/replace_text_in_file.py +195 -149
  39. janito/agent/tools/run_bash_command.py +30 -69
  40. janito/agent/tools/run_powershell_command.py +138 -105
  41. janito/agent/tools/search_text/__init__.py +1 -0
  42. janito/agent/tools/search_text/core.py +176 -0
  43. janito/agent/tools/search_text/match_lines.py +58 -0
  44. janito/agent/tools/search_text/pattern_utils.py +65 -0
  45. janito/agent/tools/search_text/traverse_directory.py +127 -0
  46. janito/agent/tools/validate_file_syntax/core.py +43 -30
  47. janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
  48. janito/agent/tools/validate_file_syntax/markdown_validator.py +77 -34
  49. janito/agent/tools_utils/action_type.py +7 -0
  50. janito/agent/tools_utils/dir_walk_utils.py +3 -2
  51. janito/agent/tools_utils/formatting.py +47 -21
  52. janito/agent/tools_utils/gitignore_utils.py +89 -40
  53. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  54. janito/agent/tools_utils/utils.py +7 -1
  55. janito/cli/_print_config.py +63 -61
  56. janito/cli/arg_parser.py +13 -12
  57. janito/cli/cli_main.py +137 -147
  58. janito/cli/config_commands.py +112 -109
  59. janito/cli/main.py +152 -174
  60. janito/cli/one_shot.py +40 -26
  61. janito/i18n/__init__.py +1 -1
  62. janito/rich_utils.py +46 -8
  63. janito/shell/commands/__init__.py +2 -4
  64. janito/shell/commands/conversation_restart.py +3 -1
  65. janito/shell/commands/edit.py +3 -0
  66. janito/shell/commands/history_view.py +3 -3
  67. janito/shell/commands/lang.py +3 -0
  68. janito/shell/commands/livelogs.py +5 -3
  69. janito/shell/commands/prompt.py +6 -0
  70. janito/shell/commands/session.py +3 -0
  71. janito/shell/commands/session_control.py +3 -0
  72. janito/shell/commands/termweb_log.py +8 -0
  73. janito/shell/commands/tools.py +3 -0
  74. janito/shell/commands/track.py +36 -0
  75. janito/shell/commands/utility.py +13 -18
  76. janito/shell/commands/verbose.py +3 -4
  77. janito/shell/input_history.py +62 -0
  78. janito/shell/main.py +160 -181
  79. janito/shell/session/config.py +83 -75
  80. janito/shell/session/manager.py +0 -21
  81. janito/shell/ui/interactive.py +97 -75
  82. janito/termweb/static/editor.css +32 -33
  83. janito/termweb/static/editor.css.bak +140 -22
  84. janito/termweb/static/editor.html +12 -7
  85. janito/termweb/static/editor.html.bak +16 -11
  86. janito/termweb/static/editor.js +94 -40
  87. janito/termweb/static/editor.js.bak +97 -65
  88. janito/termweb/static/index.html +1 -2
  89. janito/termweb/static/index.html.bak +1 -1
  90. janito/termweb/static/termweb.css +1 -22
  91. janito/termweb/static/termweb.css.bak +6 -4
  92. janito/termweb/static/termweb.js +0 -6
  93. janito/termweb/static/termweb.js.bak +1 -2
  94. janito/tests/test_rich_utils.py +44 -0
  95. janito/web/app.py +0 -75
  96. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/METADATA +61 -42
  97. janito-1.11.0.dist-info/RECORD +163 -0
  98. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/WHEEL +1 -1
  99. janito/agent/providers.py +0 -77
  100. janito/agent/tools/run_python_command.py +0 -161
  101. janito/agent/tools/search_text.py +0 -204
  102. janito/shell/commands/sum.py +0 -49
  103. janito-1.9.0.dist-info/RECORD +0 -151
  104. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/entry_points.txt +0 -0
  105. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/licenses/LICENSE +0 -0
  106. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/top_level.txt +0 -0
@@ -21,3 +21,6 @@ def handle_tools(console, args=None, shell_state=None):
21
21
  console.print(f"[red]Error loading tools: {e}[/red]")
22
22
  return
23
23
  console.print(table)
24
+
25
+
26
+ handle_tools.help_text = "List available tools"
@@ -0,0 +1,36 @@
1
+ from janito.agent.tool_use_tracker import ToolUseTracker
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+
5
+
6
+ def handle_track(console: Console, args=None, shell_state=None):
7
+ tracker = ToolUseTracker.instance()
8
+ history = tracker.get_history()
9
+ if not history:
10
+ console.print("[bold yellow]No tool usage history found.[/bold yellow]")
11
+ return
12
+ table = Table(show_header=True, header_style="bold magenta")
13
+ table.add_column("#", style="dim", width=4)
14
+ table.add_column("Tool")
15
+ table.add_column("Params/Result")
16
+ for idx, entry in enumerate(history, 1):
17
+ tool = entry["tool"]
18
+ params = entry["params"].copy()
19
+ result = entry.get("result", "")
20
+ # For create/replace file, trim content in params only
21
+ if tool in ("create_file", "replace_file") and "content" in params:
22
+ content = params["content"]
23
+ if isinstance(content, str):
24
+ lines = content.splitlines()
25
+ if len(lines) > 3:
26
+ params["content"] = (
27
+ "\n".join(lines[:2])
28
+ + f"\n... (trimmed, {len(lines)} lines total)"
29
+ )
30
+ elif len(content) > 120:
31
+ params["content"] = content[:120] + "... (trimmed)"
32
+ else:
33
+ params["content"] = content
34
+ param_result = f"{params}\n--- Result ---\n{result}" if result else str(params)
35
+ table.add_row(str(idx), tool, param_result)
36
+ console.print(table)
@@ -1,22 +1,11 @@
1
1
  def handle_help(console, **kwargs):
2
- console.print(
3
- """
4
- [bold green]Available commands:[/bold green]
5
- /exit, exit - Exit chat mode
6
- /restart - Start a new conversation
7
- /help - Show this help message
8
- /continue - Restore last saved conversation
9
- /prompt - Show the system prompt
10
- /role - Change the system role
11
- /clear - Clear the terminal screen
12
- /multi - Provide multiline input as next message
13
- /config - Show or set configuration (see: /config show, /config set local|global key=value)
14
- /termweb-logs - Show the last lines of the latest termweb logs
15
- /livelogs - Show live updates from the server log file (default: server.log)
16
- /termweb-status - Show status information about the running termweb server
17
- /verbose [on|off] - Show or set verbose mode for this session
18
- """
19
- )
2
+ from janito.shell.commands import COMMAND_HANDLERS
3
+
4
+ console.print("[bold green]Available commands:[/bold green]")
5
+ for cmd, handler in COMMAND_HANDLERS.items():
6
+ help_text = getattr(handler, "help_text", None)
7
+ if help_text:
8
+ console.print(f" {cmd} - {help_text}")
20
9
 
21
10
 
22
11
  def handle_clear(console, **kwargs):
@@ -25,9 +14,15 @@ def handle_clear(console, **kwargs):
25
14
  os.system("cls" if os.name == "nt" else "clear")
26
15
 
27
16
 
17
+ handle_clear.help_text = "Clear the terminal screen"
18
+
19
+
28
20
  def handle_multi(console, shell_state=None, **kwargs):
29
21
  console.print(
30
22
  "[bold yellow]Multiline mode activated. Provide or write your text and press Esc + Enter to submit.[/bold yellow]"
31
23
  )
32
24
  if shell_state:
33
25
  shell_state.paste_mode = True
26
+
27
+
28
+ handle_multi.help_text = "Provide multiline input as next message"
@@ -2,10 +2,6 @@ from janito.agent.runtime_config import runtime_config
2
2
 
3
3
 
4
4
  def handle_verbose(console, shell_state=None, **kwargs):
5
- """
6
- /verbose [on|off]
7
- Shows or sets verbose mode for the current shell session.
8
- """
9
5
  args = kwargs.get("args", [])
10
6
  verbose = runtime_config.get("verbose", False)
11
7
  if not args:
@@ -27,3 +23,6 @@ def handle_verbose(console, shell_state=None, **kwargs):
27
23
  )
28
24
  else:
29
25
  console.print("[bold red]Usage:[/bold red] /verbose [on|off]")
26
+
27
+
28
+ handle_verbose.help_text = "Show or set verbose mode for this session"
@@ -0,0 +1,62 @@
1
+ import os
2
+ import json
3
+ from datetime import datetime
4
+ from typing import List, Any, Dict
5
+
6
+
7
+ class UserInputHistory:
8
+ """
9
+ Handles loading, saving, and appending of user input history for the shell.
10
+ Each day's history is stored in a line-delimited JSON file (.json.log) under .janito/input_history/.
11
+ Each line is a JSON dict, e.g., {"input": ..., "ts": ...}
12
+ """
13
+
14
+ def __init__(self, history_dir=None):
15
+ self.history_dir = history_dir or os.path.join(".janito", "input_history")
16
+ os.makedirs(self.history_dir, exist_ok=True)
17
+
18
+ def _get_today_file(self):
19
+ today_str = datetime.now().strftime("%y%m%d")
20
+ return os.path.join(self.history_dir, f"{today_str}.json.log")
21
+
22
+ def load(self) -> List[Dict[str, Any]]:
23
+ """Load today's input history as a list of dicts."""
24
+ history_file = self._get_today_file()
25
+ history = []
26
+ try:
27
+ with open(history_file, "r", encoding="utf-8") as f:
28
+ for line in f:
29
+ line = line.strip()
30
+ if not line:
31
+ continue
32
+ try:
33
+ history.append(json.loads(line))
34
+ except json.JSONDecodeError:
35
+ continue
36
+ except FileNotFoundError:
37
+ pass
38
+ return history
39
+
40
+ def sanitize_surrogates(self, s):
41
+ if isinstance(s, str):
42
+ return s.encode("utf-8", errors="replace").decode("utf-8")
43
+ return s
44
+
45
+ def append(self, input_str: str):
46
+ """Append a new input as a JSON dict to today's history file."""
47
+ history_file = self._get_today_file()
48
+ input_str = self.sanitize_surrogates(input_str)
49
+ entry = {"input": input_str, "ts": datetime.now().isoformat()}
50
+ with open(history_file, "a", encoding="utf-8") as f:
51
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
52
+
53
+ def save(self, history_list: List[Any]):
54
+ """Overwrite today's history file with the given list (for compatibility)."""
55
+ history_file = self._get_today_file()
56
+ with open(history_file, "w", encoding="utf-8") as f:
57
+ for item in history_list:
58
+ if isinstance(item, dict):
59
+ f.write(json.dumps(item, ensure_ascii=False) + "\n")
60
+ else:
61
+ entry = {"input": str(item), "ts": datetime.now().isoformat()}
62
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
janito/shell/main.py CHANGED
@@ -8,84 +8,22 @@ from janito.shell.prompt.session_setup import (
8
8
  )
9
9
  from janito.shell.commands import handle_command
10
10
  from janito.agent.conversation_exceptions import EmptyResponseError, ProviderError
11
- from janito.agent.conversation_history import ConversationHistory
12
- from janito.agent.tool_use_tracker import ToolUseTracker
11
+ from janito.agent.api_exceptions import ApiError
12
+ from janito.agent.llm_conversation_history import LLMConversationHistory
13
13
  import janito.i18n as i18n
14
14
  from janito.agent.runtime_config import runtime_config
15
15
  from rich.console import Console
16
- from collections import Counter
17
16
  import os
18
17
  from janito.shell.session.manager import get_session_id
19
18
  from prompt_toolkit.formatted_text import HTML
20
19
  import time
21
-
22
-
23
- def chat_start_summary(conversation_history, console, last_usage_info):
24
- def format_tokens(n):
25
- if n is None:
26
- return "-"
27
- if n >= 1_000_000:
28
- return f"{n/1_000_000:.2f}m"
29
- elif n >= 1_000:
30
- return f"{n/1_000:.2f}k"
31
- return str(n)
32
-
33
- num_messages = len(conversation_history)
34
- roles = [m.get("role") for m in conversation_history.get_messages()]
35
- role_counts = {role: roles.count(role) for role in set(roles)}
36
- roles_str = ", ".join(
37
- f"[bold]{role}[/]: {count}" for role, count in role_counts.items()
38
- )
39
- stats_lines = [
40
- f"[cyan]Messages:[/] [bold]{num_messages}[/]",
41
- f"[cyan]Roles:[/] {roles_str}",
42
- ]
43
- # Use last_usage_info for tokens
44
- if last_usage_info:
45
- prompt_tokens = last_usage_info.get("prompt_tokens")
46
- completion_tokens = last_usage_info.get("completion_tokens")
47
- total_tokens = last_usage_info.get("total_tokens")
48
- tokens_parts = []
49
- if prompt_tokens is not None:
50
- tokens_parts.append(f"Prompt: [bold]{format_tokens(prompt_tokens)}[/]")
51
- if completion_tokens is not None:
52
- tokens_parts.append(
53
- f"Completion: [bold]{format_tokens(completion_tokens)}[/]"
54
- )
55
- if total_tokens is not None:
56
- tokens_parts.append(f"Total: [bold]{format_tokens(total_tokens)}[/]")
57
- if tokens_parts:
58
- stats_lines.append(f"[cyan]Tokens:[/] {', '.join(tokens_parts)}")
59
-
60
- # Add global tool usage stats
61
- try:
62
- tool_history = ToolUseTracker().get_history()
63
- if tool_history:
64
- tool_counts = Counter(
65
- entry["tool"] for entry in tool_history if "tool" in entry
66
- )
67
- tools_str = ", ".join(
68
- f"[bold]{tool}[/]: {count}" for tool, count in tool_counts.items()
69
- )
70
- stats_lines.append(f"[cyan]Tools used:[/] {tools_str}")
71
- except Exception:
72
- pass # Fail silently if tracker is unavailable
73
-
74
- # Print all stats in a single line, no panel
75
- # Print stats in a single line, but tokens info on a separate line if present
76
- if len(stats_lines) > 2:
77
- console.print(" | ".join(stats_lines[:2]))
78
- console.print(stats_lines[2])
79
- if len(stats_lines) > 3:
80
- console.print(" | ".join(stats_lines[3:]))
81
- else:
82
- console.print(" | ".join(stats_lines))
20
+ from janito.shell.input_history import UserInputHistory
83
21
 
84
22
 
85
23
  @dataclass
86
24
  class ShellState:
87
25
  mem_history: Any = field(default_factory=InMemoryHistory)
88
- conversation_history: Any = field(default_factory=lambda: ConversationHistory())
26
+ conversation_history: Any = field(default_factory=lambda: LLMConversationHistory())
89
27
  last_usage_info: Dict[str, int] = field(
90
28
  default_factory=lambda: {
91
29
  "prompt_tokens": 0,
@@ -100,44 +38,26 @@ class ShellState:
100
38
  livereload_stderr_path: Optional[str] = None
101
39
  paste_mode: bool = False
102
40
  profile_manager: Optional[Any] = None
41
+ user_input_history: Optional[Any] = None
103
42
 
104
43
 
105
44
  # Track the active prompt session for cleanup
106
45
  active_prompt_session = None
107
46
 
108
47
 
109
- def start_chat_shell(
110
- profile_manager,
111
- continue_session=False,
112
- session_id=None,
113
- max_rounds=100,
114
- termweb_stdout_path=None,
115
- termweb_stderr_path=None,
116
- livereload_stdout_path=None,
117
- livereload_stderr_path=None,
118
- ):
119
-
120
- i18n.set_locale(runtime_config.get("lang", "en"))
121
- global active_prompt_session
122
- agent = profile_manager.agent
123
- message_handler = RichMessageHandler()
124
- console = message_handler.console
125
-
126
- # Print session id at start
48
+ def load_session(shell_state, continue_session, session_id, profile_manager):
127
49
  from janito.shell.session.manager import load_conversation_by_session_id
128
50
 
129
- shell_state = ShellState()
130
- shell_state.profile_manager = profile_manager
131
51
  if continue_session and session_id:
132
52
  try:
133
53
  messages, prompts, usage = load_conversation_by_session_id(session_id)
134
54
  except FileNotFoundError as e:
135
- console.print(f"[bold red]{str(e)}[/bold red]")
136
- return
137
- # Initialize ConversationHistory with loaded messages
138
- shell_state.conversation_history = ConversationHistory(messages)
55
+ shell_state.profile_manager.agent.message_handler.console.print(
56
+ f"[bold red]{str(e)}[/bold red]"
57
+ )
58
+ return False
59
+ shell_state.conversation_history = LLMConversationHistory(messages)
139
60
  conversation_history = shell_state.conversation_history
140
- # Always refresh the system prompt in the loaded history
141
61
  found = False
142
62
  for msg in conversation_history.get_messages():
143
63
  if msg.get("role") == "system":
@@ -148,12 +68,9 @@ def start_chat_shell(
148
68
  conversation_history.set_system_message(
149
69
  profile_manager.system_prompt_template
150
70
  )
151
- # Optionally set prompts/usage if needed
152
71
  shell_state.last_usage_info = usage or {}
153
72
  else:
154
73
  conversation_history = shell_state.conversation_history
155
- # Add system prompt if needed (skip in vanilla mode)
156
-
157
74
  if (
158
75
  profile_manager.system_prompt_template
159
76
  and (
@@ -167,29 +84,18 @@ def start_chat_shell(
167
84
  conversation_history.set_system_message(
168
85
  profile_manager.system_prompt_template
169
86
  )
170
- mem_history = shell_state.mem_history
87
+ return True
171
88
 
172
- def last_usage_info_ref():
173
- return shell_state.last_usage_info
174
-
175
- last_elapsed = shell_state.last_elapsed
176
-
177
- print_welcome_message(console, continue_id=session_id if continue_session else None)
178
-
179
- session = setup_prompt_session(
180
- lambda: conversation_history.get_messages(),
181
- last_usage_info_ref,
182
- last_elapsed,
183
- mem_history,
184
- profile_manager,
185
- agent,
186
- lambda: conversation_history,
187
- )
188
- active_prompt_session = session
189
89
 
90
+ def handle_prompt_loop(
91
+ shell_state, session, profile_manager, agent, max_rounds, session_id
92
+ ):
93
+ global active_prompt_session
94
+ conversation_history = shell_state.conversation_history
95
+ message_handler = RichMessageHandler()
96
+ console = message_handler.console
190
97
  while True:
191
98
  try:
192
-
193
99
  if shell_state.paste_mode:
194
100
  user_input = session.prompt("Multiline> ", multiline=True)
195
101
  was_paste_mode = True
@@ -203,11 +109,10 @@ def start_chat_shell(
203
109
  console.print("\n[bold red]Exiting...[/bold red]")
204
110
  break
205
111
  except KeyboardInterrupt:
206
- console.print() # Move to next line
112
+ console.print()
207
113
  try:
208
114
  confirm = (
209
115
  session.prompt(
210
- # Use <inputline> for full-line blue background, <prompt> for icon only
211
116
  HTML(
212
117
  "<inputline>Do you really want to exit? (y/n): </inputline>"
213
118
  )
@@ -230,10 +135,8 @@ def start_chat_shell(
230
135
  break
231
136
  else:
232
137
  continue
233
-
234
138
  cmd_input = user_input.strip().lower()
235
139
  if not was_paste_mode and (cmd_input.startswith("/") or cmd_input == "exit"):
236
- # Treat both '/exit' and 'exit' as commands
237
140
  result = handle_command(
238
141
  user_input.strip(),
239
142
  console,
@@ -245,77 +148,153 @@ def start_chat_shell(
245
148
  )
246
149
  break
247
150
  continue
248
-
249
151
  if not user_input.strip():
250
152
  continue
251
-
252
- mem_history.append_string(user_input)
153
+ shell_state.mem_history.append_string(user_input)
154
+ shell_state.user_input_history.append(user_input)
253
155
  conversation_history.add_message({"role": "user", "content": user_input})
156
+ handle_chat(shell_state, profile_manager, agent, max_rounds, session_id)
157
+ # Save conversation history after exiting
158
+ save_conversation_history(conversation_history, session_id)
159
+
160
+
161
+ def handle_keyboard_interrupt(conversation_history, message_handler):
162
+ removed_count = 0
163
+ while (
164
+ conversation_history.last_message()
165
+ and conversation_history.last_message().get("role") != "assistant"
166
+ ):
167
+ conversation_history.remove_last_message()
168
+ removed_count += 1
169
+ # Remove the assistant message itself, if present
170
+ if (
171
+ conversation_history.last_message()
172
+ and conversation_history.last_message().get("role") == "assistant"
173
+ ):
174
+ conversation_history.remove_last_message()
175
+ removed_count += 1
176
+ message_handler.handle_message(
177
+ {
178
+ "type": "info",
179
+ "message": f"\nLast turn cleared due to interruption. Returning to prompt. (removed {removed_count} last msgs)\n",
180
+ }
181
+ )
254
182
 
255
- start_time = time.time()
256
-
257
- # No need to propagate verbose; ToolExecutor and others fetch from runtime_config
258
-
259
- # Clear the screen before starting LLM conversation
260
- console = Console()
261
- console.clear()
262
-
263
- # Print a summary of the current conversation history
264
-
265
- chat_start_summary(conversation_history, console, shell_state.last_usage_info)
266
-
267
- try:
268
- response = profile_manager.agent.chat(
269
- conversation_history,
270
- max_rounds=max_rounds,
271
- message_handler=message_handler,
272
- spinner=True,
273
- )
274
- except KeyboardInterrupt:
275
- message_handler.handle_message(
276
- {"type": "info", "message": "Request interrupted. Returning to prompt."}
277
- )
278
- continue
279
- except ProviderError as e:
280
- message_handler.handle_message(
281
- {"type": "error", "message": f"Provider error: {e}"}
282
- )
283
- continue
284
- except EmptyResponseError as e:
285
- message_handler.handle_message({"type": "error", "message": f"Error: {e}"})
286
- continue
287
- last_elapsed = time.time() - start_time
288
-
289
- usage = response.get("usage")
290
- if usage:
291
- for k in ("prompt_tokens", "completion_tokens", "total_tokens"):
292
- shell_state.last_usage_info[k] = usage.get(k, 0)
293
-
294
- # --- Ensure assistant and tool messages are added to ConversationHistory ---
295
- # If the last message is not an assistant/tool, add the response content
296
- content = response.get("content")
297
- if content and (
298
- len(conversation_history) == 0
299
- or conversation_history.get_messages()[-1].get("role") != "assistant"
300
- ):
301
- conversation_history.add_message({"role": "assistant", "content": content})
302
- # Optionally, add tool messages if present in response (extend here if needed)
303
- # ---------------------------------------------------------------------------
304
-
305
- # --- Save conversation history after each assistant reply ---
306
- session_id_to_save = session_id if session_id else get_session_id()
307
- history_dir = os.path.join(os.path.expanduser("~"), ".janito", "chat_history")
308
- os.makedirs(history_dir, exist_ok=True)
309
- history_path = os.path.join(history_dir, f"{session_id_to_save}.json")
310
- conversation_history.to_json_file(history_path)
311
- # -----------------------------------------------------------
312
183
 
313
- # After exiting the main loop, print restart info if conversation has >1 message
184
+ def handle_chat_error(message_handler, error_type, error):
185
+ if error_type == "ProviderError":
186
+ message_handler.handle_message(
187
+ {"type": "error", "message": f"Provider error: {error}"}
188
+ )
189
+ elif error_type == "EmptyResponseError":
190
+ message_handler.handle_message({"type": "error", "message": f"Error: {error}"})
191
+ elif error_type == "ApiError":
192
+ message_handler.handle_message({"type": "error", "message": str(error)})
193
+ elif error_type == "Exception":
194
+ import traceback
195
+
196
+ tb = traceback.format_exc()
197
+ message_handler.handle_message(
198
+ {
199
+ "type": "error",
200
+ "message": f"Unexpected error: {error}\n{tb}\nReturning to prompt.",
201
+ }
202
+ )
203
+
204
+
205
+ def handle_chat(shell_state, profile_manager, agent, max_rounds, session_id):
206
+ conversation_history = shell_state.conversation_history
207
+ message_handler = RichMessageHandler()
208
+ console = message_handler.console
209
+ start_time = time.time()
210
+ try:
211
+ response = profile_manager.agent.chat(
212
+ conversation_history,
213
+ max_rounds=max_rounds,
214
+ message_handler=message_handler,
215
+ spinner=True,
216
+ )
217
+ except KeyboardInterrupt:
218
+ handle_keyboard_interrupt(conversation_history, message_handler)
219
+ return
220
+ except ProviderError as e:
221
+ handle_chat_error(message_handler, "ProviderError", e)
222
+ return
223
+ except EmptyResponseError as e:
224
+ handle_chat_error(message_handler, "EmptyResponseError", e)
225
+ return
226
+ except ApiError as e:
227
+ handle_chat_error(message_handler, "ApiError", e)
228
+ return
229
+ except Exception as e:
230
+ handle_chat_error(message_handler, "Exception", e)
231
+ return
232
+ shell_state.last_elapsed = time.time() - start_time
233
+ usage = response.get("usage")
234
+ if usage:
235
+ for k in ("prompt_tokens", "completion_tokens", "total_tokens"):
236
+ shell_state.last_usage_info[k] = usage.get(k, 0)
237
+ content = response.get("content")
238
+ if content and (
239
+ len(conversation_history) == 0
240
+ or conversation_history.get_messages()[-1].get("role") != "assistant"
241
+ ):
242
+ conversation_history.add_message({"role": "assistant", "content": content})
243
+
244
+
245
+ def save_conversation_history(conversation_history, session_id):
246
+ from janito.shell.session.manager import get_session_id
314
247
 
315
- # --- Save conversation history to .janito/chat_history/(session_id).json ---
316
- session_id_to_save = session_id if session_id else get_session_id()
317
248
  history_dir = os.path.join(os.path.expanduser("~"), ".janito", "chat_history")
318
249
  os.makedirs(history_dir, exist_ok=True)
250
+ session_id_to_save = session_id if session_id else get_session_id()
319
251
  history_path = os.path.join(history_dir, f"{session_id_to_save}.json")
320
252
  conversation_history.to_json_file(history_path)
321
- # -------------------------------------------------------------------------
253
+
254
+
255
+ def start_chat_shell(
256
+ profile_manager,
257
+ continue_session=False,
258
+ session_id=None,
259
+ max_rounds=100,
260
+ termweb_stdout_path=None,
261
+ termweb_stderr_path=None,
262
+ livereload_stdout_path=None,
263
+ livereload_stderr_path=None,
264
+ ):
265
+ i18n.set_locale(runtime_config.get("lang", "en"))
266
+ global active_prompt_session
267
+ agent = profile_manager.agent
268
+ message_handler = RichMessageHandler()
269
+ console = message_handler.console
270
+ console.clear()
271
+ shell_state = ShellState()
272
+ shell_state.profile_manager = profile_manager
273
+ user_input_history = UserInputHistory()
274
+ user_input_dicts = user_input_history.load()
275
+ mem_history = shell_state.mem_history
276
+ for item in user_input_dicts:
277
+ if isinstance(item, dict) and "input" in item:
278
+ mem_history.append_string(item["input"])
279
+ shell_state.user_input_history = user_input_history
280
+ if not load_session(shell_state, continue_session, session_id, profile_manager):
281
+ return
282
+
283
+ def last_usage_info_ref():
284
+ return shell_state.last_usage_info
285
+
286
+ last_elapsed = shell_state.last_elapsed
287
+ print_welcome_message(console, continue_id=session_id if continue_session else None)
288
+ session = setup_prompt_session(
289
+ lambda: shell_state.conversation_history.get_messages(),
290
+ last_usage_info_ref,
291
+ last_elapsed,
292
+ mem_history,
293
+ profile_manager,
294
+ agent,
295
+ lambda: shell_state.conversation_history,
296
+ )
297
+ active_prompt_session = session
298
+ handle_prompt_loop(
299
+ shell_state, session, profile_manager, agent, max_rounds, session_id
300
+ )