yaicli 0.4.0__py3-none-any.whl → 0.5.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.
- pyproject.toml +5 -3
- yaicli/chat.py +396 -0
- yaicli/cli.py +250 -251
- yaicli/client.py +385 -0
- yaicli/config.py +31 -24
- yaicli/console.py +2 -2
- yaicli/const.py +28 -2
- yaicli/entry.py +68 -39
- yaicli/exceptions.py +8 -36
- yaicli/functions/__init__.py +39 -0
- yaicli/functions/buildin/execute_shell_command.py +47 -0
- yaicli/printer.py +145 -225
- yaicli/render.py +1 -1
- yaicli/role.py +231 -0
- yaicli/schemas.py +31 -0
- yaicli/tools.py +103 -0
- yaicli/utils.py +5 -2
- {yaicli-0.4.0.dist-info → yaicli-0.5.0.dist-info}/METADATA +164 -87
- yaicli-0.5.0.dist-info/RECORD +24 -0
- {yaicli-0.4.0.dist-info → yaicli-0.5.0.dist-info}/entry_points.txt +1 -1
- yaicli/chat_manager.py +0 -290
- yaicli/providers/__init__.py +0 -34
- yaicli/providers/base.py +0 -51
- yaicli/providers/cohere.py +0 -136
- yaicli/providers/openai.py +0 -176
- yaicli/roles.py +0 -276
- yaicli-0.4.0.dist-info/RECORD +0 -23
- {yaicli-0.4.0.dist-info → yaicli-0.5.0.dist-info}/WHEEL +0 -0
- {yaicli-0.4.0.dist-info → yaicli-0.5.0.dist-info}/licenses/LICENSE +0 -0
yaicli/cli.py
CHANGED
@@ -2,9 +2,9 @@ import subprocess
|
|
2
2
|
import sys
|
3
3
|
import time
|
4
4
|
import traceback
|
5
|
-
from os
|
5
|
+
from os import devnull
|
6
6
|
from pathlib import Path
|
7
|
-
from typing import
|
7
|
+
from typing import Optional, Union
|
8
8
|
|
9
9
|
import typer
|
10
10
|
from prompt_toolkit import PromptSession, prompt
|
@@ -16,96 +16,90 @@ from rich.padding import Padding
|
|
16
16
|
from rich.panel import Panel
|
17
17
|
from rich.prompt import Prompt
|
18
18
|
|
19
|
-
from
|
20
|
-
from
|
21
|
-
from
|
22
|
-
from
|
19
|
+
from .chat import Chat, FileChatManager, chat_mgr
|
20
|
+
from .client import LitellmClient, ChatMessage
|
21
|
+
from .config import cfg
|
22
|
+
from .console import get_console
|
23
|
+
from .const import (
|
23
24
|
CHAT_MODE,
|
24
25
|
CMD_CLEAR,
|
25
26
|
CMD_DELETE_CHAT,
|
26
27
|
CMD_EXIT,
|
28
|
+
CMD_HELP,
|
27
29
|
CMD_HISTORY,
|
28
30
|
CMD_LIST_CHATS,
|
29
31
|
CMD_LOAD_CHAT,
|
30
32
|
CMD_MODE,
|
31
33
|
CMD_SAVE_CHAT,
|
32
|
-
|
33
|
-
DEFAULT_INTERACTIVE_ROUND,
|
34
|
+
CONFIG_PATH,
|
34
35
|
DEFAULT_OS_NAME,
|
35
36
|
DEFAULT_SHELL_NAME,
|
36
37
|
EXEC_MODE,
|
38
|
+
HISTORY_FILE,
|
37
39
|
TEMP_MODE,
|
38
40
|
DefaultRoleNames,
|
39
41
|
)
|
40
|
-
from
|
41
|
-
from
|
42
|
-
from
|
43
|
-
from
|
44
|
-
from
|
42
|
+
from .exceptions import ChatSaveError
|
43
|
+
from .history import LimitedFileHistory
|
44
|
+
from .printer import Printer
|
45
|
+
from .role import Role, RoleManager, role_mgr
|
46
|
+
from .utils import detect_os, detect_shell, filter_command
|
45
47
|
|
46
48
|
|
47
49
|
class CLI:
|
48
|
-
HISTORY_FILE = Path("~/.yaicli_history").expanduser()
|
49
|
-
|
50
50
|
def __init__(
|
51
51
|
self,
|
52
52
|
verbose: bool = False,
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
role: Optional[str] = None,
|
53
|
+
role: str = DefaultRoleNames.DEFAULT,
|
54
|
+
chat_manager: Optional[FileChatManager] = None,
|
55
|
+
role_manager: Optional[RoleManager] = None,
|
56
|
+
client=None,
|
58
57
|
):
|
59
|
-
|
60
|
-
|
61
|
-
|
58
|
+
self.verbose: bool = verbose
|
59
|
+
# --role can specify a role when enter interactive chat
|
60
|
+
# TAB will switch between role and shell
|
61
|
+
self.init_role: str = role
|
62
|
+
self.role_name: str = role
|
63
|
+
|
62
64
|
self.console = get_console()
|
63
|
-
self.
|
64
|
-
self.
|
65
|
-
self.
|
66
|
-
self.
|
67
|
-
self.
|
65
|
+
self.chat_manager = chat_manager or chat_mgr
|
66
|
+
self.role_manager = role_manager or role_mgr
|
67
|
+
self.role: Role = self.role_manager.get_role(self.role_name)
|
68
|
+
self.printer = Printer()
|
69
|
+
self.client = client or LitellmClient(verbose=self.verbose)
|
68
70
|
|
69
|
-
|
70
|
-
self.role_manager = RoleManager() # Singleton
|
71
|
+
self.bindings = KeyBindings()
|
71
72
|
|
72
|
-
|
73
|
-
if not self.role_manager.role_exists(self.role):
|
74
|
-
self.console.print(f"Role '{self.role}' not found, using default role.", style="yellow")
|
75
|
-
self.role = DefaultRoleNames.DEFAULT.value
|
73
|
+
self.current_mode: str = TEMP_MODE
|
76
74
|
|
77
|
-
|
78
|
-
self.history = []
|
79
|
-
self.interactive_max_history = self.config.get("INTERACTIVE_MAX_HISTORY", DEFAULT_INTERACTIVE_ROUND)
|
80
|
-
self.chat_title = None
|
75
|
+
self.interactive_round = cfg["INTERACTIVE_ROUND"]
|
81
76
|
self.chat_start_time = None
|
82
77
|
self.is_temp_session = True
|
78
|
+
self.chat = Chat(title="", history=[])
|
83
79
|
|
84
80
|
# Get and create chat history directory from configuration
|
85
|
-
self.chat_history_dir = Path(
|
86
|
-
self.chat_history_dir.
|
87
|
-
|
88
|
-
# Initialize chat manager
|
89
|
-
self.chat_manager = chat_manager or FileChatManager()
|
81
|
+
self.chat_history_dir = Path(cfg["CHAT_HISTORY_DIR"])
|
82
|
+
# if not self.chat_history_dir.exists():
|
83
|
+
# self.chat_history_dir.mkdir(parents=True, exist_ok=True)
|
90
84
|
|
91
85
|
# Detect OS and Shell if set to auto
|
92
|
-
if
|
93
|
-
|
94
|
-
if
|
95
|
-
|
86
|
+
if cfg["OS_NAME"] == DEFAULT_OS_NAME:
|
87
|
+
cfg["OS_NAME"] = detect_os(cfg)
|
88
|
+
if cfg["SHELL_NAME"] == DEFAULT_SHELL_NAME:
|
89
|
+
cfg["SHELL_NAME"] = detect_shell(cfg)
|
96
90
|
|
97
91
|
if self.verbose:
|
92
|
+
# Print verbose configuration
|
98
93
|
self.console.print("Loading Configuration:", style="bold cyan")
|
99
94
|
self.console.print(f"Config file path: {CONFIG_PATH}")
|
100
|
-
for key, value in
|
95
|
+
for key, value in cfg.items():
|
101
96
|
display_value = "****" if key == "API_KEY" and value else value
|
102
|
-
self.console.print(f" {key:<
|
103
|
-
self.console.print(f"Current role: {self.
|
104
|
-
self.console.print(Markdown("---", code_theme=
|
105
|
-
|
106
|
-
self.api_client = api_client or create_api_client(self.config, self.console, self.verbose)
|
107
|
-
self.printer = printer or Printer(self.config, self.console, self.verbose, markdown=True)
|
97
|
+
self.console.print(f" {key:<20}: {display_value}")
|
98
|
+
self.console.print(f"Current role: {self.role_name}")
|
99
|
+
self.console.print(Markdown("---", code_theme=cfg["CODE_THEME"]))
|
108
100
|
|
101
|
+
# Disable prompt_toolkit warning when use non-tty input,
|
102
|
+
# e.g. when use pipe or redirect
|
109
103
|
_origin_stderr = None
|
110
104
|
if not sys.stdin.isatty():
|
111
105
|
_origin_stderr = sys.stderr
|
@@ -118,46 +112,63 @@ class CLI:
|
|
118
112
|
sys.stderr.close()
|
119
113
|
sys.stderr = _origin_stderr
|
120
114
|
|
121
|
-
def
|
115
|
+
def set_role(self, role_name: str) -> None:
|
116
|
+
self.role_name = role_name
|
117
|
+
self.role = self.role_manager.get_role(role_name)
|
118
|
+
if role_name in (DefaultRoleNames.CODER, DefaultRoleNames.SHELL):
|
119
|
+
cfg["ENABLE_FUNCTIONS"] = False
|
120
|
+
if role_name == DefaultRoleNames.CODER:
|
121
|
+
self.printer = Printer(content_markdown=False)
|
122
|
+
elif role_name == DefaultRoleNames.SHELL:
|
123
|
+
self.current_mode = EXEC_MODE
|
124
|
+
|
125
|
+
@classmethod
|
126
|
+
def evaluate_role_name(cls, code: bool = False, shell: bool = False, role: str = ""):
|
127
|
+
"""
|
128
|
+
Judge the role based on the code, shell, and role options.
|
129
|
+
Code and shell are highest priority, then role, then default.
|
130
|
+
"""
|
131
|
+
if code is True:
|
132
|
+
return DefaultRoleNames.CODER
|
133
|
+
if shell is True:
|
134
|
+
return DefaultRoleNames.SHELL
|
135
|
+
if role:
|
136
|
+
return role
|
137
|
+
return DefaultRoleNames.DEFAULT
|
138
|
+
|
139
|
+
def get_prompt_tokens(self) -> list[tuple[str, str]]:
|
122
140
|
"""Return prompt tokens for current mode"""
|
123
141
|
mode_icon = "💬" if self.current_mode == CHAT_MODE else "🚀" if self.current_mode == EXEC_MODE else "📝"
|
124
142
|
return [("class:qmark", f" {mode_icon} "), ("class:prompt", "> ")]
|
125
143
|
|
126
144
|
def _check_history_len(self) -> None:
|
127
145
|
"""Check history length and remove the oldest messages if necessary"""
|
128
|
-
target_len = self.
|
129
|
-
if len(self.history) > target_len:
|
130
|
-
self.history = self.history[-target_len:]
|
146
|
+
target_len = self.interactive_round * 2
|
147
|
+
if len(self.chat.history) > target_len:
|
148
|
+
self.chat.history = self.chat.history[-target_len:]
|
131
149
|
if self.verbose:
|
132
|
-
self.console.print(f"
|
133
|
-
|
134
|
-
# ------------------- Role Command Methods -------------------
|
135
|
-
def set_role(self, role: str) -> None:
|
136
|
-
"""Set the current role for the assistant"""
|
137
|
-
if not self.role_manager.role_exists(role):
|
138
|
-
self.console.print(f"Role '{role}' not found.", style="bold red")
|
139
|
-
return
|
140
|
-
|
141
|
-
self.role = role
|
142
|
-
if self.role == DefaultRoleNames.CODER:
|
143
|
-
self.printer = Printer(self.config, self.console, self.verbose, content_markdown=False)
|
150
|
+
self.console.print(f"Dialogue trimmed to {target_len} messages.", style="dim")
|
144
151
|
|
145
152
|
# ------------------- Chat Command Methods -------------------
|
146
|
-
def _save_chat(self, title:
|
153
|
+
def _save_chat(self, title: Union[str, None] = None) -> None:
|
147
154
|
"""Save current chat history to a file using session manager."""
|
148
|
-
|
155
|
+
# Update title if provided
|
156
|
+
if title:
|
157
|
+
self.chat.title = title
|
149
158
|
|
150
|
-
|
151
|
-
|
159
|
+
# Save chat and get the saved title back
|
160
|
+
try:
|
161
|
+
saved_title = self.chat_manager.save_chat(self.chat)
|
162
|
+
except ChatSaveError as e:
|
163
|
+
self.console.print(f"Failed to save chat: {e}", style="red")
|
152
164
|
return
|
153
165
|
|
154
166
|
# Session list will be refreshed automatically by the save method
|
155
167
|
self.console.print(f"Chat saved as: {saved_title}", style="bold green")
|
156
168
|
|
157
|
-
#
|
169
|
+
# Mark session as persistent if it was temporary
|
158
170
|
if self.is_temp_session:
|
159
171
|
self.is_temp_session = False
|
160
|
-
self.chat_title = saved_title
|
161
172
|
self.chat_start_time = int(time.time())
|
162
173
|
self.console.print(
|
163
174
|
"Session is now marked as persistent and will be auto-saved on exit.", style="bold green"
|
@@ -165,7 +176,7 @@ class CLI:
|
|
165
176
|
|
166
177
|
def _list_chats(self) -> None:
|
167
178
|
"""List all saved chat sessions using session manager."""
|
168
|
-
chats: list[
|
179
|
+
chats: list[Chat] = self.chat_manager.list_chats()
|
169
180
|
|
170
181
|
if not chats:
|
171
182
|
self.console.print("No saved chats found.", style="yellow")
|
@@ -173,9 +184,9 @@ class CLI:
|
|
173
184
|
|
174
185
|
self.console.print("Saved Chats:", style="bold underline")
|
175
186
|
for chat in chats:
|
176
|
-
index = chat
|
177
|
-
title = chat
|
178
|
-
date = chat.
|
187
|
+
index = chat.idx
|
188
|
+
title = chat.title
|
189
|
+
date = chat.date
|
179
190
|
|
180
191
|
if date:
|
181
192
|
self.console.print(f"[dim]{index}.[/dim] [bold blue]{title}[/bold blue] - {date}")
|
@@ -186,7 +197,7 @@ class CLI:
|
|
186
197
|
"""Force refresh the chat list."""
|
187
198
|
self.chat_manager.refresh_chats()
|
188
199
|
|
189
|
-
def _load_chat_by_index(self, index:
|
200
|
+
def _load_chat_by_index(self, index: str) -> bool:
|
190
201
|
"""Load a chat session by its index using session manager."""
|
191
202
|
if not self.chat_manager.validate_chat_index(index):
|
192
203
|
self.console.print("Invalid chat index.", style="bold red")
|
@@ -198,15 +209,14 @@ class CLI:
|
|
198
209
|
self.console.print("Invalid chat index or chat not found.", style="bold red")
|
199
210
|
return False
|
200
211
|
|
201
|
-
self.
|
202
|
-
self.
|
203
|
-
self.chat_start_time = chat_data.get("timestamp", int(time.time()))
|
212
|
+
self.chat = chat_data
|
213
|
+
self.chat_start_time = chat_data.date
|
204
214
|
self.is_temp_session = False
|
205
215
|
|
206
|
-
self.console.print(f"Loaded chat: {self.
|
216
|
+
self.console.print(f"Loaded chat: {self.chat.title}", style="bold green")
|
207
217
|
return True
|
208
218
|
|
209
|
-
def _delete_chat_by_index(self, index:
|
219
|
+
def _delete_chat_by_index(self, index: str) -> bool:
|
210
220
|
"""Delete a chat session by its index using session manager."""
|
211
221
|
if not self.chat_manager.validate_chat_index(index):
|
212
222
|
self.console.print("Invalid chat index.", style="bold red")
|
@@ -218,34 +228,41 @@ class CLI:
|
|
218
228
|
self.console.print("Invalid chat index or chat not found.", style="bold red")
|
219
229
|
return False
|
220
230
|
|
221
|
-
if
|
222
|
-
self.console.print(f"
|
231
|
+
if chat_data.path is None:
|
232
|
+
self.console.print(f"Chat has no associated file to delete: {chat_data.title}", style="bold red")
|
233
|
+
return False
|
234
|
+
|
235
|
+
if self.chat_manager.delete_chat(chat_data.path):
|
236
|
+
self.console.print(f"Deleted chat: {chat_data.title}", style="bold green")
|
223
237
|
return True
|
224
238
|
else:
|
225
|
-
self.console.print(f"Failed to delete chat: {chat_data
|
239
|
+
self.console.print(f"Failed to delete chat: {chat_data.title}", style="bold red")
|
226
240
|
return False
|
227
241
|
|
228
242
|
# ------------------- Special commands -------------------
|
229
|
-
def _handle_special_commands(self, user_input: str) ->
|
230
|
-
"""Handle special command return: True-continue loop, False-exit loop,
|
243
|
+
def _handle_special_commands(self, user_input: str) -> Union[bool, str]:
|
244
|
+
"""Handle special command return: True-continue loop, False-exit loop, str-non-special command"""
|
231
245
|
command = user_input.lower().strip()
|
246
|
+
if command in CMD_HELP:
|
247
|
+
self.print_help()
|
248
|
+
return True
|
232
249
|
if command == CMD_EXIT:
|
233
250
|
return False
|
234
251
|
if command == CMD_CLEAR and self.current_mode == CHAT_MODE:
|
235
|
-
self.history.clear()
|
252
|
+
self.chat.history.clear()
|
236
253
|
self.console.print("Chat history cleared", style="bold yellow")
|
237
254
|
return True
|
238
255
|
if command == CMD_HISTORY:
|
239
|
-
if not self.history:
|
256
|
+
if not self.chat.history:
|
240
257
|
self.console.print("History is empty.", style="yellow")
|
241
258
|
else:
|
242
259
|
self.console.print("Chat History:", style="bold underline")
|
243
|
-
for i in range(0, len(self.history), 2):
|
244
|
-
user_msg = self.history[i]
|
245
|
-
assistant_msg = self.history[i + 1] if (i + 1) < len(self.history) else None
|
246
|
-
self.console.print(f"[dim]{i // 2 + 1}[/dim] [bold blue]User:[/bold blue] {user_msg
|
260
|
+
for i in range(0, len(self.chat.history), 2):
|
261
|
+
user_msg = self.chat.history[i]
|
262
|
+
assistant_msg = self.chat.history[i + 1] if (i + 1) < len(self.chat.history) else None
|
263
|
+
self.console.print(f"[dim]{i // 2 + 1}[/dim] [bold blue]User:[/bold blue] {user_msg.content}")
|
247
264
|
if assistant_msg:
|
248
|
-
md = Markdown(assistant_msg
|
265
|
+
md = Markdown(assistant_msg.content, code_theme=cfg["CODE_THEME"])
|
249
266
|
padded_md = Padding(md, (0, 0, 0, 4))
|
250
267
|
self.console.print(" Assistant:", style="bold green")
|
251
268
|
self.console.print(padded_md)
|
@@ -254,7 +271,7 @@ class CLI:
|
|
254
271
|
# Handle /save command - optional title parameter
|
255
272
|
if command.startswith(CMD_SAVE_CHAT):
|
256
273
|
parts = command.split(maxsplit=1)
|
257
|
-
title = parts[1] if len(parts) > 1 else self.
|
274
|
+
title = parts[1] if len(parts) > 1 else self.chat.title
|
258
275
|
self._save_chat(title)
|
259
276
|
return True
|
260
277
|
|
@@ -263,8 +280,7 @@ class CLI:
|
|
263
280
|
parts = command.split(maxsplit=1)
|
264
281
|
if len(parts) == 2 and parts[1].isdigit():
|
265
282
|
# Try to parse as an index first
|
266
|
-
index
|
267
|
-
self._load_chat_by_index(index=index)
|
283
|
+
self._load_chat_by_index(index=parts[1])
|
268
284
|
else:
|
269
285
|
self.console.print(f"Usage: {CMD_LOAD_CHAT} <index>", style="yellow")
|
270
286
|
self._list_chats()
|
@@ -274,8 +290,7 @@ class CLI:
|
|
274
290
|
if command.startswith(CMD_DELETE_CHAT):
|
275
291
|
parts = command.split(maxsplit=1)
|
276
292
|
if len(parts) == 2 and parts[1].isdigit():
|
277
|
-
index
|
278
|
-
self._delete_chat_by_index(index=index)
|
293
|
+
self._delete_chat_by_index(index=parts[1])
|
279
294
|
else:
|
280
295
|
self.console.print(f"Usage: {CMD_DELETE_CHAT} <index>", style="yellow")
|
281
296
|
self._list_chats()
|
@@ -293,71 +308,25 @@ class CLI:
|
|
293
308
|
new_mode = parts[1]
|
294
309
|
if self.current_mode != new_mode:
|
295
310
|
self.current_mode = new_mode
|
311
|
+
self.set_role(DefaultRoleNames.SHELL if self.current_mode == EXEC_MODE else self.init_role)
|
296
312
|
else:
|
297
313
|
self.console.print(f"Already in {self.current_mode} mode.", style="yellow")
|
298
314
|
else:
|
299
315
|
self.console.print(f"Usage: {CMD_MODE} {CHAT_MODE}|{EXEC_MODE}", style="yellow")
|
300
316
|
return True
|
301
|
-
return
|
302
|
-
|
303
|
-
def _confirm_and_execute(self, raw_content: str) -> None:
|
304
|
-
"""Review, edit and execute the command"""
|
305
|
-
cmd = filter_command(raw_content)
|
306
|
-
if not cmd:
|
307
|
-
self.console.print("No command generated or command is empty.", style="bold red")
|
308
|
-
return
|
309
|
-
self.console.print(
|
310
|
-
Panel(cmd, title="Suggest Command", title_align="left", border_style="bold magenta", expand=False)
|
311
|
-
)
|
312
|
-
_input = Prompt.ask(
|
313
|
-
r"Execute command? \[e]dit, \[y]es, \[n]o",
|
314
|
-
choices=["y", "n", "e"],
|
315
|
-
default="n",
|
316
|
-
case_sensitive=False,
|
317
|
-
show_choices=False,
|
318
|
-
)
|
319
|
-
executed_cmd = None
|
320
|
-
if _input == "y":
|
321
|
-
executed_cmd = cmd
|
322
|
-
elif _input == "e":
|
323
|
-
try:
|
324
|
-
edited_cmd = prompt("Edit command: ", default=cmd).strip()
|
325
|
-
if edited_cmd and edited_cmd != cmd:
|
326
|
-
executed_cmd = edited_cmd
|
327
|
-
elif edited_cmd:
|
328
|
-
executed_cmd = cmd
|
329
|
-
else:
|
330
|
-
self.console.print("Execution cancelled.", style="yellow")
|
331
|
-
except EOFError:
|
332
|
-
self.console.print("\nEdit cancelled.", style="yellow")
|
333
|
-
return
|
334
|
-
if executed_cmd:
|
335
|
-
self.console.print("--- Executing --- ", style="bold green")
|
336
|
-
try:
|
337
|
-
subprocess.call(executed_cmd, shell=True)
|
338
|
-
except Exception as e:
|
339
|
-
self.console.print(f"[red]Failed to execute command: {e}[/red]")
|
340
|
-
self.console.print("--- Finished ---", style="bold green")
|
341
|
-
elif _input != "e":
|
342
|
-
self.console.print("Execution cancelled.", style="yellow")
|
317
|
+
return user_input
|
343
318
|
|
344
|
-
|
345
|
-
def get_system_prompt(self) -> str:
|
346
|
-
"""Get the system prompt based on current role and mode"""
|
347
|
-
# Use the role manager to get the system prompt
|
348
|
-
return self.role_manager.get_system_prompt(self.role)
|
349
|
-
|
350
|
-
def _build_messages(self, user_input: str) -> List[dict]:
|
319
|
+
def _build_messages(self, user_input: str) -> list[ChatMessage]:
|
351
320
|
"""Build message list for LLM API"""
|
352
321
|
# Create the message list with system prompt
|
353
|
-
messages = [
|
322
|
+
messages = [ChatMessage(role="system", content=self.role.prompt)]
|
354
323
|
|
355
324
|
# Add previous conversation if available
|
356
|
-
for msg in self.history:
|
325
|
+
for msg in self.chat.history:
|
357
326
|
messages.append(msg)
|
358
327
|
|
359
328
|
# Add user input
|
360
|
-
messages.append(
|
329
|
+
messages.append(ChatMessage(role="user", content=user_input))
|
361
330
|
return messages
|
362
331
|
|
363
332
|
def _handle_llm_response(self, user_input: str) -> Optional[str]:
|
@@ -373,26 +342,19 @@ class CLI:
|
|
373
342
|
messages = self._build_messages(user_input)
|
374
343
|
if self.verbose:
|
375
344
|
self.console.print(messages)
|
376
|
-
|
345
|
+
if self.role != DefaultRoleNames.CODER:
|
346
|
+
self.console.print("Assistant:", style="bold green")
|
377
347
|
try:
|
378
|
-
|
379
|
-
|
380
|
-
content,
|
381
|
-
else:
|
382
|
-
content, reasoning = self.api_client.completion(messages)
|
383
|
-
self.printer.display_normal(content, reasoning, not is_code_mode)
|
384
|
-
|
385
|
-
if content is not None:
|
386
|
-
# Add only the content (not reasoning) to history
|
387
|
-
self.history.extend(
|
388
|
-
[{"role": "user", "content": user_input}, {"role": "assistant", "content": content}]
|
389
|
-
)
|
390
|
-
self._check_history_len()
|
391
|
-
return content
|
348
|
+
response = self.client.completion(messages, stream=cfg["STREAM"])
|
349
|
+
if cfg["STREAM"]:
|
350
|
+
content, _ = self.printer.display_stream(response, messages)
|
392
351
|
else:
|
393
|
-
|
352
|
+
content, _ = self.printer.display_normal(response, messages)
|
353
|
+
|
354
|
+
# Just return the content, message addition is handled in _process_user_input
|
355
|
+
return content if content is not None else None
|
394
356
|
except Exception as e:
|
395
|
-
self.console.print(f"
|
357
|
+
self.console.print(f"Error processing LLM response: {e}", style="red")
|
396
358
|
if self.verbose:
|
397
359
|
traceback.print_exc()
|
398
360
|
return None
|
@@ -406,11 +368,83 @@ class CLI:
|
|
406
368
|
if content is None:
|
407
369
|
return True
|
408
370
|
|
371
|
+
# Update chat history using Chat's add_message method
|
372
|
+
self.chat.add_message("user", user_input)
|
373
|
+
self.chat.add_message("assistant", content)
|
374
|
+
|
375
|
+
self._check_history_len()
|
376
|
+
|
409
377
|
if self.current_mode == EXEC_MODE:
|
410
378
|
self._confirm_and_execute(content)
|
411
379
|
return True
|
412
380
|
|
381
|
+
def _confirm_and_execute(self, raw_content: str) -> None:
|
382
|
+
"""Review, edit and execute the command"""
|
383
|
+
cmd = filter_command(raw_content)
|
384
|
+
if not cmd:
|
385
|
+
self.console.print("No command generated or command is empty.", style="bold red")
|
386
|
+
return
|
387
|
+
self.console.print(
|
388
|
+
Panel(cmd, title="Suggest Command", title_align="left", border_style="bold magenta", expand=False)
|
389
|
+
)
|
390
|
+
_input = Prompt.ask(
|
391
|
+
r"Execute command? \[e]dit, \[y]es, \[n]o",
|
392
|
+
choices=["y", "n", "e"],
|
393
|
+
default="y",
|
394
|
+
case_sensitive=False,
|
395
|
+
show_choices=False,
|
396
|
+
)
|
397
|
+
executed_cmd = None
|
398
|
+
if _input == "y":
|
399
|
+
executed_cmd = cmd
|
400
|
+
elif _input == "e":
|
401
|
+
try:
|
402
|
+
edited_cmd = prompt("Edit command: ", default=cmd).strip()
|
403
|
+
if edited_cmd and edited_cmd != cmd:
|
404
|
+
executed_cmd = edited_cmd
|
405
|
+
elif edited_cmd:
|
406
|
+
executed_cmd = cmd
|
407
|
+
else:
|
408
|
+
self.console.print("Execution cancelled.", style="yellow")
|
409
|
+
except EOFError:
|
410
|
+
self.console.print("\nEdit cancelled.", style="yellow")
|
411
|
+
return
|
412
|
+
if executed_cmd:
|
413
|
+
self.console.print("Executing...", style="bold green")
|
414
|
+
try:
|
415
|
+
subprocess.call(executed_cmd, shell=True)
|
416
|
+
except Exception as e:
|
417
|
+
self.console.print(f"Failed to execute command: {e}", style="red")
|
418
|
+
elif _input != "e":
|
419
|
+
self.console.print("Execution cancelled.", style="yellow")
|
420
|
+
|
413
421
|
# ------------------- REPL Methods -------------------
|
422
|
+
def prepare_chat_loop(self) -> None:
|
423
|
+
"""Setup key bindings and history for interactive modes."""
|
424
|
+
self.current_mode = CHAT_MODE
|
425
|
+
self._setup_key_bindings()
|
426
|
+
HISTORY_FILE.touch(exist_ok=True)
|
427
|
+
|
428
|
+
# Set up the prompt session with command history
|
429
|
+
try:
|
430
|
+
self.session = PromptSession(
|
431
|
+
key_bindings=self.bindings,
|
432
|
+
history=LimitedFileHistory(HISTORY_FILE, max_entries=self.interactive_round),
|
433
|
+
auto_suggest=AutoSuggestFromHistory() if cfg.get("AUTO_SUGGEST", True) else None,
|
434
|
+
enable_history_search=True,
|
435
|
+
)
|
436
|
+
except Exception as e:
|
437
|
+
self.console.print(f"Error initializing prompt session history: {e}", style="red")
|
438
|
+
self.session = PromptSession(key_bindings=self.bindings)
|
439
|
+
|
440
|
+
def _setup_key_bindings(self) -> None:
|
441
|
+
"""Setup keyboard shortcuts (e.g., TAB for mode switching)."""
|
442
|
+
|
443
|
+
@self.bindings.add(Keys.ControlI) # TAB
|
444
|
+
def _(event: KeyPressEvent) -> None:
|
445
|
+
self.current_mode = EXEC_MODE if self.current_mode == CHAT_MODE else CHAT_MODE
|
446
|
+
self.set_role(DefaultRoleNames.SHELL if self.current_mode == EXEC_MODE else self.init_role)
|
447
|
+
|
414
448
|
def _print_welcome_message(self) -> None:
|
415
449
|
"""Prints the initial welcome banner and instructions."""
|
416
450
|
self.console.print(
|
@@ -430,10 +464,14 @@ class CLI:
|
|
430
464
|
self.console.print("Current: [bold yellow]Temporary Session[/bold yellow] (use /save to make persistent)")
|
431
465
|
else:
|
432
466
|
self.console.print(
|
433
|
-
f"Current: [bold green]Persistent Session[/bold green]{f': {self.
|
467
|
+
f"Current: [bold green]Persistent Session[/bold green]{f': {self.chat.title}' if self.chat.title else ''}"
|
434
468
|
)
|
469
|
+
self.print_help()
|
435
470
|
|
471
|
+
def print_help(self):
|
436
472
|
self.console.print("Press [bold yellow]TAB[/bold yellow] to switch mode")
|
473
|
+
help_cmd = "|".join(CMD_HELP)
|
474
|
+
self.console.print(f"{help_cmd:<19}: Show help message")
|
437
475
|
self.console.print(f"{CMD_CLEAR:<19}: Clear chat history")
|
438
476
|
self.console.print(f"{CMD_HISTORY:<19}: Show chat history")
|
439
477
|
self.console.print(f"{CMD_LIST_CHATS:<19}: List saved chats")
|
@@ -452,98 +490,59 @@ class CLI:
|
|
452
490
|
"""Run the main Read-Eval-Print Loop (REPL)."""
|
453
491
|
self.prepare_chat_loop()
|
454
492
|
self._print_welcome_message()
|
493
|
+
|
494
|
+
# Main REPL loop
|
455
495
|
while True:
|
456
|
-
self.console.print(Markdown("---", code_theme=
|
496
|
+
self.console.print(Markdown("---", code_theme=cfg["CODE_THEME"]))
|
457
497
|
try:
|
498
|
+
# Get user input
|
458
499
|
user_input = self.session.prompt(self.get_prompt_tokens)
|
459
500
|
user_input = user_input.strip()
|
460
501
|
if not user_input:
|
461
502
|
continue
|
462
|
-
|
463
|
-
|
503
|
+
|
504
|
+
# Handle special commands
|
505
|
+
_continue = self._handle_special_commands(user_input)
|
506
|
+
if _continue is False: # Exit command
|
464
507
|
break
|
465
|
-
if
|
508
|
+
if _continue is True: # Other special command
|
509
|
+
continue
|
510
|
+
|
511
|
+
# Process regular chat input
|
512
|
+
try:
|
513
|
+
if not self._process_user_input(user_input):
|
514
|
+
break
|
515
|
+
except KeyboardInterrupt:
|
516
|
+
self.console.print("KeyboardInterrupt", style="yellow")
|
466
517
|
continue
|
467
|
-
if not self._process_user_input(user_input):
|
468
|
-
break
|
469
518
|
except (KeyboardInterrupt, EOFError):
|
470
519
|
break
|
471
520
|
|
472
521
|
# Auto-save chat history when exiting if there are messages and not a temporary session
|
473
|
-
if not self.is_temp_session:
|
474
|
-
self._save_chat(self.
|
522
|
+
if not self.is_temp_session and self.chat.history:
|
523
|
+
self._save_chat(self.chat.title)
|
475
524
|
|
476
525
|
self.console.print("\nExiting YAICLI... Goodbye!", style="bold green")
|
477
526
|
|
478
|
-
def
|
479
|
-
"""
|
480
|
-
self.
|
481
|
-
self.
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
def _setup_key_bindings(self) -> None:
|
498
|
-
"""Setup keyboard shortcuts (e.g., TAB for mode switching)."""
|
499
|
-
|
500
|
-
@self.bindings.add(Keys.ControlI) # TAB
|
501
|
-
def _(event: KeyPressEvent) -> None:
|
502
|
-
self.current_mode = EXEC_MODE if self.current_mode == CHAT_MODE else CHAT_MODE
|
503
|
-
self.role = DefaultRoleNames.SHELL if self.current_mode == EXEC_MODE else self.init_role
|
504
|
-
|
505
|
-
def _run_once(self, input: str, shell: bool) -> None:
|
506
|
-
"""Run a single command (non-interactive)."""
|
507
|
-
self.current_mode = EXEC_MODE if shell else TEMP_MODE
|
508
|
-
if not self.config.get("API_KEY"):
|
509
|
-
self.console.print("[bold red]Error:[/bold red] API key not found.")
|
510
|
-
raise typer.Exit(code=1)
|
511
|
-
|
512
|
-
content = self._handle_llm_response(input)
|
513
|
-
|
514
|
-
if content is None:
|
515
|
-
raise typer.Exit(code=1)
|
516
|
-
|
517
|
-
if shell:
|
518
|
-
self._confirm_and_execute(content)
|
519
|
-
|
520
|
-
# ------------------- Main Entry Point -------------------
|
521
|
-
def run(
|
522
|
-
self,
|
523
|
-
chat: bool,
|
524
|
-
shell: bool,
|
525
|
-
input: Optional[str],
|
526
|
-
role: Optional[str | Literal[DefaultRoleNames.DEFAULT]] = None,
|
527
|
-
) -> None:
|
528
|
-
"""Run the CLI in the appropriate mode with the selected role."""
|
529
|
-
self.set_role(role or self.role)
|
530
|
-
|
531
|
-
# Now handle normal operation
|
532
|
-
if shell:
|
533
|
-
# Set mode to shell
|
534
|
-
self.role = DefaultRoleNames.SHELL
|
535
|
-
if input:
|
536
|
-
self._run_once(input, shell=True)
|
537
|
-
else:
|
538
|
-
self.console.print("No prompt provided for shell mode.", style="yellow")
|
539
|
-
elif chat:
|
540
|
-
# Start interactive chat mode
|
541
|
-
self.current_mode = CHAT_MODE
|
542
|
-
self.chat_title = input if input else None
|
543
|
-
self.prepare_chat_loop()
|
527
|
+
def _run_once(self, user_input: str, shell: bool = False, code: bool = False) -> None:
|
528
|
+
"""Handle default mode"""
|
529
|
+
self.set_role(self.evaluate_role_name(code, shell, self.init_role))
|
530
|
+
self._process_user_input(user_input)
|
531
|
+
|
532
|
+
def run(self, chat: bool = False, shell: bool = False, code: bool = False, user_input: Optional[str] = None):
|
533
|
+
if not user_input and not chat:
|
534
|
+
self.console.print("No input provided.", style="bold red")
|
535
|
+
raise typer.Abort()
|
536
|
+
|
537
|
+
if chat:
|
538
|
+
# If user provided a title, try to load that chat
|
539
|
+
if user_input and isinstance(user_input, str):
|
540
|
+
loaded_chat = self.chat_manager.load_chat_by_title(user_input)
|
541
|
+
if loaded_chat:
|
542
|
+
self.chat = loaded_chat
|
543
|
+
self.is_temp_session = False
|
544
|
+
# Run the interactive chat REPL
|
544
545
|
self._run_repl()
|
545
|
-
elif input:
|
546
|
-
# Run once with the given prompt
|
547
|
-
self._run_once(input, shell=False)
|
548
546
|
else:
|
549
|
-
|
547
|
+
# Run in single-use mode
|
548
|
+
self._run_once(user_input or "", shell=shell, code=code)
|