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