yaicli 0.2.0__py3-none-any.whl → 0.3.1__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 +1 -1
- yaicli/api.py +19 -27
- yaicli/chat_manager.py +46 -19
- yaicli/cli.py +101 -68
- yaicli/config.py +17 -1
- yaicli/console.py +66 -0
- yaicli/const.py +51 -6
- yaicli/entry.py +173 -50
- yaicli/exceptions.py +46 -0
- yaicli/printer.py +113 -57
- yaicli/render.py +19 -0
- yaicli/roles.py +248 -0
- yaicli/utils.py +21 -2
- {yaicli-0.2.0.dist-info → yaicli-0.3.1.dist-info}/METADATA +200 -97
- yaicli-0.3.1.dist-info/RECORD +20 -0
- yaicli-0.2.0.dist-info/RECORD +0 -16
- {yaicli-0.2.0.dist-info → yaicli-0.3.1.dist-info}/WHEEL +0 -0
- {yaicli-0.2.0.dist-info → yaicli-0.3.1.dist-info}/entry_points.txt +0 -0
- {yaicli-0.2.0.dist-info → yaicli-0.3.1.dist-info}/licenses/LICENSE +0 -0
pyproject.toml
CHANGED
yaicli/api.py
CHANGED
@@ -8,33 +8,18 @@ from rich.console import Console
|
|
8
8
|
from yaicli.const import (
|
9
9
|
DEFAULT_BASE_URL,
|
10
10
|
DEFAULT_COMPLETION_PATH,
|
11
|
-
DEFAULT_MAX_TOKENS,
|
12
11
|
DEFAULT_MODEL,
|
13
|
-
DEFAULT_TEMPERATURE,
|
14
|
-
DEFAULT_TIMEOUT,
|
15
|
-
DEFAULT_TOP_P,
|
16
12
|
EventTypeEnum,
|
17
13
|
)
|
18
14
|
|
19
15
|
|
20
16
|
def parse_stream_line(line: Union[bytes, str], console: Console, verbose: bool) -> Optional[dict]:
|
21
17
|
"""(Helper Function) Parse a single line from the SSE stream response."""
|
22
|
-
|
23
|
-
if isinstance(line, bytes):
|
24
|
-
try:
|
25
|
-
line_str = line.decode("utf-8")
|
26
|
-
except UnicodeDecodeError:
|
27
|
-
if verbose:
|
28
|
-
console.print(f"Warning: Could not decode stream line bytes: {line!r}", style="yellow")
|
29
|
-
return None
|
30
|
-
elif isinstance(line, str):
|
31
|
-
line_str = line
|
32
|
-
else:
|
33
|
-
# Handle unexpected line types
|
18
|
+
if not isinstance(line, (bytes, str)):
|
34
19
|
if verbose:
|
35
|
-
console.print(f"Warning: Received
|
20
|
+
console.print(f"Warning: Received non-string/bytes line: {line!r}", style="yellow")
|
36
21
|
return None
|
37
|
-
|
22
|
+
line_str: str = line.decode("utf-8") if isinstance(line, bytes) else line
|
38
23
|
line_str = line_str.strip()
|
39
24
|
if not line_str or not line_str.startswith("data: "):
|
40
25
|
return None
|
@@ -69,8 +54,8 @@ class ApiClient:
|
|
69
54
|
self.completion_path = str(config.get("COMPLETION_PATH", DEFAULT_COMPLETION_PATH))
|
70
55
|
self.api_key = str(config.get("API_KEY", ""))
|
71
56
|
self.model = str(config.get("MODEL", DEFAULT_MODEL))
|
72
|
-
self.timeout = self.config
|
73
|
-
self.client = client or httpx.Client(timeout=self.config
|
57
|
+
self.timeout = self.config["TIMEOUT"]
|
58
|
+
self.client = client or httpx.Client(timeout=self.config["TIMEOUT"])
|
74
59
|
|
75
60
|
def _prepare_request_body(self, messages: List[Dict[str, str]], stream: bool) -> Dict[str, Any]:
|
76
61
|
"""Prepare the common request body for API calls."""
|
@@ -78,9 +63,12 @@ class ApiClient:
|
|
78
63
|
"messages": messages,
|
79
64
|
"model": self.model,
|
80
65
|
"stream": stream,
|
81
|
-
"temperature": self.config
|
82
|
-
"top_p": self.config
|
83
|
-
"max_tokens": self.config
|
66
|
+
"temperature": self.config["TEMPERATURE"],
|
67
|
+
"top_p": self.config["TOP_P"],
|
68
|
+
"max_tokens": self.config[
|
69
|
+
"MAX_TOKENS"
|
70
|
+
], # Openai: This value is now deprecated in favor of max_completion_tokens
|
71
|
+
"max_completion_tokens": self.config["MAX_TOKENS"],
|
84
72
|
}
|
85
73
|
|
86
74
|
def _handle_api_error(self, e: httpx.HTTPError) -> None:
|
@@ -105,7 +93,11 @@ class ApiClient:
|
|
105
93
|
|
106
94
|
def get_headers(self) -> Dict[str, str]:
|
107
95
|
"""Get the request headers."""
|
108
|
-
return {
|
96
|
+
return {
|
97
|
+
"Authorization": f"Bearer {self.api_key}",
|
98
|
+
"Content-Type": "application/json",
|
99
|
+
"X-Title": "Yaicli",
|
100
|
+
}
|
109
101
|
|
110
102
|
def _process_completion_response(self, response_json: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
|
111
103
|
"""Process the JSON response from a non-streamed completion request."""
|
@@ -190,7 +182,7 @@ class ApiClient:
|
|
190
182
|
"""Process a single chunk from the stream and yield events with updated reasoning state.
|
191
183
|
|
192
184
|
Args:
|
193
|
-
parsed_data: The parsed JSON data from a
|
185
|
+
parsed_data: The parsed JSON data from a streamline
|
194
186
|
in_reasoning: Whether we're currently in a reasoning state
|
195
187
|
|
196
188
|
Yields:
|
@@ -273,7 +265,7 @@ class ApiClient:
|
|
273
265
|
yield self._handle_http_error(e)
|
274
266
|
return
|
275
267
|
|
276
|
-
# Process the
|
268
|
+
# Process the streamline by line
|
277
269
|
for line in response.iter_lines():
|
278
270
|
parsed_data = parse_stream_line(line, self.console, self.verbose)
|
279
271
|
if parsed_data is None:
|
@@ -315,7 +307,7 @@ class ApiClient:
|
|
315
307
|
# reasoning_content: deepseek/infi-ai
|
316
308
|
# reasoning: openrouter
|
317
309
|
# <think> block implementation not in here
|
318
|
-
for key in ("reasoning_content", "reasoning"
|
310
|
+
for key in ("reasoning_content", "reasoning"):
|
319
311
|
# Check if the key exists and its value is a non-empty string
|
320
312
|
value = delta.get(key)
|
321
313
|
if isinstance(value, str) and value:
|
yaicli/chat_manager.py
CHANGED
@@ -3,17 +3,30 @@ import time
|
|
3
3
|
from abc import ABC, abstractmethod
|
4
4
|
from datetime import datetime
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import Any, Dict, List, Optional, TypedDict
|
6
|
+
from typing import Any, Dict, List, Optional, TypedDict, Union
|
7
7
|
|
8
|
-
from rich import get_console
|
9
8
|
from rich.console import Console
|
10
9
|
|
11
|
-
from yaicli.config import Config
|
10
|
+
from yaicli.config import Config, cfg
|
11
|
+
from yaicli.console import get_console
|
12
|
+
from yaicli.utils import option_callback
|
13
|
+
|
14
|
+
|
15
|
+
class ChatFileInfo(TypedDict):
|
16
|
+
"""Chat info, parse chat filename and store metadata"""
|
17
|
+
|
18
|
+
index: int
|
19
|
+
path: str
|
20
|
+
title: str
|
21
|
+
date: str
|
22
|
+
timestamp: int
|
12
23
|
|
13
24
|
|
14
25
|
class ChatsMap(TypedDict):
|
15
|
-
|
16
|
-
|
26
|
+
"""Chat info cache for chat manager"""
|
27
|
+
|
28
|
+
title: Dict[str, ChatFileInfo]
|
29
|
+
index: Dict[int, ChatFileInfo]
|
17
30
|
|
18
31
|
|
19
32
|
class ChatManager(ABC):
|
@@ -30,7 +43,7 @@ class ChatManager(ABC):
|
|
30
43
|
pass
|
31
44
|
|
32
45
|
@abstractmethod
|
33
|
-
def list_chats(self) -> List[
|
46
|
+
def list_chats(self) -> List[ChatFileInfo]:
|
34
47
|
"""List all saved chats and return the chat list"""
|
35
48
|
pass
|
36
49
|
|
@@ -40,12 +53,12 @@ class ChatManager(ABC):
|
|
40
53
|
pass
|
41
54
|
|
42
55
|
@abstractmethod
|
43
|
-
def load_chat_by_index(self, index: int) ->
|
56
|
+
def load_chat_by_index(self, index: int) -> Union[ChatFileInfo, Dict]:
|
44
57
|
"""Load a chat by index and return the chat data"""
|
45
58
|
pass
|
46
59
|
|
47
60
|
@abstractmethod
|
48
|
-
def load_chat_by_title(self, title: str) ->
|
61
|
+
def load_chat_by_title(self, title: str) -> Union[ChatFileInfo, Dict]:
|
49
62
|
"""Load a chat by title and return the chat data"""
|
50
63
|
pass
|
51
64
|
|
@@ -63,13 +76,14 @@ class ChatManager(ABC):
|
|
63
76
|
class FileChatManager(ChatManager):
|
64
77
|
"""File system based chat manager implementation"""
|
65
78
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
79
|
+
console: Console = get_console()
|
80
|
+
config: Config = cfg
|
81
|
+
chat_dir = Path(config["CHAT_HISTORY_DIR"])
|
82
|
+
max_saved_chats = config["MAX_SAVED_CHATS"]
|
83
|
+
chat_dir.mkdir(parents=True, exist_ok=True)
|
84
|
+
|
85
|
+
def __init__(self):
|
71
86
|
self._chats_map: Optional[ChatsMap] = None # Cache for chat map
|
72
|
-
self.console = console or get_console()
|
73
87
|
|
74
88
|
@property
|
75
89
|
def chats_map(self) -> ChatsMap:
|
@@ -78,6 +92,18 @@ class FileChatManager(ChatManager):
|
|
78
92
|
self._load_chats()
|
79
93
|
return self._chats_map or {"index": {}, "title": {}}
|
80
94
|
|
95
|
+
@classmethod
|
96
|
+
@option_callback
|
97
|
+
def print_list_option(cls, _: Any):
|
98
|
+
"""Print the list of chats"""
|
99
|
+
cls.console.print("Finding Chats...")
|
100
|
+
c = -1
|
101
|
+
for c, file in enumerate(sorted(cls.chat_dir.glob("*.json"), key=lambda f: f.stat().st_mtime)):
|
102
|
+
info: ChatFileInfo = cls._parse_filename(file, c + 1)
|
103
|
+
cls.console.print(f"{c + 1}. {info['title']} ({info['date']})")
|
104
|
+
if c == -1:
|
105
|
+
cls.console.print("No chats found", style="dim")
|
106
|
+
|
81
107
|
def make_chat_title(self, prompt: Optional[str] = None) -> str:
|
82
108
|
"""Make a chat title from a given full prompt"""
|
83
109
|
if prompt:
|
@@ -93,7 +119,8 @@ class FileChatManager(ChatManager):
|
|
93
119
|
"""Force refresh the chat list from disk"""
|
94
120
|
self._load_chats()
|
95
121
|
|
96
|
-
|
122
|
+
@staticmethod
|
123
|
+
def _parse_filename(chat_file: Path, index: int) -> ChatFileInfo:
|
97
124
|
"""Parse a chat filename and extract metadata"""
|
98
125
|
# filename: "20250421-214005-title-meaning of life"
|
99
126
|
filename = chat_file.stem
|
@@ -155,7 +182,7 @@ class FileChatManager(ChatManager):
|
|
155
182
|
|
156
183
|
self._chats_map = chats_map
|
157
184
|
|
158
|
-
def list_chats(self) -> List[
|
185
|
+
def list_chats(self) -> List[ChatFileInfo]:
|
159
186
|
"""List all saved chats and return the chat list"""
|
160
187
|
return list(self.chats_map["index"].values())
|
161
188
|
|
@@ -203,7 +230,7 @@ class FileChatManager(ChatManager):
|
|
203
230
|
self.console.print(f"Error saving chat '{save_title}': {e}", style="dim")
|
204
231
|
return ""
|
205
232
|
|
206
|
-
def _load_chat_data(self, chat_info: Optional[
|
233
|
+
def _load_chat_data(self, chat_info: Optional[ChatFileInfo]) -> Union[ChatFileInfo, Dict]:
|
207
234
|
"""Common method to load chat data from a chat info dict"""
|
208
235
|
if not chat_info:
|
209
236
|
return {}
|
@@ -228,14 +255,14 @@ class FileChatManager(ChatManager):
|
|
228
255
|
self.console.print(f"Error loading chat from {chat_info['path']}: {e}", style="dim")
|
229
256
|
return {}
|
230
257
|
|
231
|
-
def load_chat_by_index(self, index: int) ->
|
258
|
+
def load_chat_by_index(self, index: int) -> Union[ChatFileInfo, Dict]:
|
232
259
|
"""Load a chat by index and return the chat data"""
|
233
260
|
if not self.validate_chat_index(index):
|
234
261
|
return {}
|
235
262
|
chat_info = self.chats_map.get("index", {}).get(index)
|
236
263
|
return self._load_chat_data(chat_info)
|
237
264
|
|
238
|
-
def load_chat_by_title(self, title: str) ->
|
265
|
+
def load_chat_by_title(self, title: str) -> Union[ChatFileInfo, Dict]:
|
239
266
|
"""Load a chat by title and return the chat data"""
|
240
267
|
chat_info = self.chats_map.get("title", {}).get(title)
|
241
268
|
return self._load_chat_data(chat_info)
|
yaicli/cli.py
CHANGED
@@ -4,22 +4,22 @@ import time
|
|
4
4
|
import traceback
|
5
5
|
from os.path import devnull
|
6
6
|
from pathlib import Path
|
7
|
-
from typing import List, Optional, Tuple
|
7
|
+
from typing import List, Literal, Optional, Tuple
|
8
8
|
|
9
9
|
import typer
|
10
10
|
from prompt_toolkit import PromptSession, prompt
|
11
11
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
12
12
|
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
|
13
13
|
from prompt_toolkit.keys import Keys
|
14
|
-
from rich import get_console
|
15
14
|
from rich.markdown import Markdown
|
16
15
|
from rich.padding import Padding
|
17
16
|
from rich.panel import Panel
|
18
17
|
from rich.prompt import Prompt
|
19
18
|
|
20
19
|
from yaicli.api import ApiClient
|
21
|
-
from yaicli.chat_manager import ChatManager, FileChatManager
|
22
|
-
from yaicli.config import CONFIG_PATH, Config
|
20
|
+
from yaicli.chat_manager import ChatFileInfo, ChatManager, FileChatManager
|
21
|
+
from yaicli.config import CONFIG_PATH, Config, cfg
|
22
|
+
from yaicli.console import get_console
|
23
23
|
from yaicli.const import (
|
24
24
|
CHAT_MODE,
|
25
25
|
CMD_CLEAR,
|
@@ -33,14 +33,14 @@ from yaicli.const import (
|
|
33
33
|
DEFAULT_CODE_THEME,
|
34
34
|
DEFAULT_INTERACTIVE_ROUND,
|
35
35
|
DEFAULT_OS_NAME,
|
36
|
-
DEFAULT_PROMPT,
|
37
36
|
DEFAULT_SHELL_NAME,
|
38
37
|
EXEC_MODE,
|
39
|
-
SHELL_PROMPT,
|
40
38
|
TEMP_MODE,
|
39
|
+
DefaultRoleNames,
|
41
40
|
)
|
42
41
|
from yaicli.history import LimitedFileHistory
|
43
42
|
from yaicli.printer import Printer
|
43
|
+
from yaicli.roles import RoleManager
|
44
44
|
from yaicli.utils import detect_os, detect_shell, filter_command
|
45
45
|
|
46
46
|
|
@@ -54,14 +54,26 @@ class CLI:
|
|
54
54
|
api_client: Optional[ApiClient] = None,
|
55
55
|
printer: Optional[Printer] = None,
|
56
56
|
chat_manager: Optional[ChatManager] = None,
|
57
|
+
role: Optional[str] = None,
|
57
58
|
):
|
59
|
+
# General settings
|
58
60
|
self.verbose = verbose
|
59
61
|
self.stdin = stdin
|
60
62
|
self.console = get_console()
|
61
63
|
self.bindings = KeyBindings()
|
62
|
-
self.config: Config =
|
64
|
+
self.config: Config = cfg
|
63
65
|
self.current_mode: str = TEMP_MODE
|
66
|
+
self.role: str = role or DefaultRoleNames.DEFAULT.value
|
64
67
|
|
68
|
+
# Initialize role manager
|
69
|
+
self.role_manager = RoleManager()
|
70
|
+
|
71
|
+
# Validate role
|
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
|
75
|
+
|
76
|
+
# Interactive chat mode settings
|
65
77
|
self.history = []
|
66
78
|
self.interactive_max_history = self.config.get("INTERACTIVE_MAX_HISTORY", DEFAULT_INTERACTIVE_ROUND)
|
67
79
|
self.chat_title = None
|
@@ -72,8 +84,8 @@ class CLI:
|
|
72
84
|
self.chat_history_dir = Path(self.config["CHAT_HISTORY_DIR"])
|
73
85
|
self.chat_history_dir.mkdir(parents=True, exist_ok=True)
|
74
86
|
|
75
|
-
# Initialize
|
76
|
-
self.chat_manager = chat_manager or FileChatManager(
|
87
|
+
# Initialize chat manager
|
88
|
+
self.chat_manager = chat_manager or FileChatManager()
|
77
89
|
|
78
90
|
# Detect OS and Shell if set to auto
|
79
91
|
if self.config.get("OS_NAME") == DEFAULT_OS_NAME:
|
@@ -87,10 +99,11 @@ class CLI:
|
|
87
99
|
for key, value in self.config.items():
|
88
100
|
display_value = "****" if key == "API_KEY" and value else value
|
89
101
|
self.console.print(f" {key:<17}: {display_value}")
|
102
|
+
self.console.print(f"Current role: {self.role}")
|
90
103
|
self.console.print(Markdown("---", code_theme=self.config.get("CODE_THEME", DEFAULT_CODE_THEME)))
|
91
104
|
|
92
105
|
self.api_client = api_client or ApiClient(self.config, self.console, self.verbose)
|
93
|
-
self.printer = printer or Printer(self.config, self.console, self.verbose)
|
106
|
+
self.printer = printer or Printer(self.config, self.console, self.verbose, markdown=True)
|
94
107
|
|
95
108
|
_origin_stderr = None
|
96
109
|
if not sys.stdin.isatty():
|
@@ -110,13 +123,25 @@ class CLI:
|
|
110
123
|
return [("class:qmark", f" {mode_icon} "), ("class:prompt", "> ")]
|
111
124
|
|
112
125
|
def _check_history_len(self) -> None:
|
113
|
-
"""Check history length and remove oldest messages if necessary"""
|
126
|
+
"""Check history length and remove the oldest messages if necessary"""
|
114
127
|
target_len = self.interactive_max_history * 2
|
115
128
|
if len(self.history) > target_len:
|
116
129
|
self.history = self.history[-target_len:]
|
117
130
|
if self.verbose:
|
118
131
|
self.console.print(f"History trimmed to {target_len} messages.", style="dim")
|
119
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)
|
143
|
+
|
144
|
+
# ------------------- Chat Command Methods -------------------
|
120
145
|
def _save_chat(self, title: Optional[str] = None) -> None:
|
121
146
|
"""Save current chat history to a file using session manager."""
|
122
147
|
saved_title = self.chat_manager.save_chat(self.history, title)
|
@@ -139,7 +164,7 @@ class CLI:
|
|
139
164
|
|
140
165
|
def _list_chats(self) -> None:
|
141
166
|
"""List all saved chat sessions using session manager."""
|
142
|
-
chats = self.chat_manager.list_chats()
|
167
|
+
chats: list[ChatFileInfo] = self.chat_manager.list_chats()
|
143
168
|
|
144
169
|
if not chats:
|
145
170
|
self.console.print("No saved chats found.", style="yellow")
|
@@ -199,6 +224,7 @@ class CLI:
|
|
199
224
|
self.console.print(f"Failed to delete chat: {chat_data['title']}", style="bold red")
|
200
225
|
return False
|
201
226
|
|
227
|
+
# ------------------- Special commands -------------------
|
202
228
|
def _handle_special_commands(self, user_input: str) -> Optional[bool]:
|
203
229
|
"""Handle special command return: True-continue loop, False-exit loop, None-non-special command"""
|
204
230
|
command = user_input.lower().strip()
|
@@ -266,8 +292,6 @@ class CLI:
|
|
266
292
|
new_mode = parts[1]
|
267
293
|
if self.current_mode != new_mode:
|
268
294
|
self.current_mode = new_mode
|
269
|
-
mode_name = "Chat" if self.current_mode == CHAT_MODE else "Exec"
|
270
|
-
self.console.print(f"Switched to [bold yellow]{mode_name}[/bold yellow] mode")
|
271
295
|
else:
|
272
296
|
self.console.print(f"Already in {self.current_mode} mode.", style="yellow")
|
273
297
|
else:
|
@@ -316,21 +340,24 @@ class CLI:
|
|
316
340
|
elif _input != "e":
|
317
341
|
self.console.print("Execution cancelled.", style="yellow")
|
318
342
|
|
343
|
+
# ------------------- LLM Methods -------------------
|
319
344
|
def get_system_prompt(self) -> str:
|
320
|
-
"""
|
321
|
-
|
322
|
-
|
323
|
-
if self.verbose and stdin:
|
324
|
-
self.console.print("Added STDIN to prompt", style="dim")
|
325
|
-
return prompt_template.format(_os=self.config["OS_NAME"], _shell=self.config["SHELL_NAME"]) + stdin
|
345
|
+
"""Get the system prompt based on current role and mode"""
|
346
|
+
# Use the role manager to get the system prompt
|
347
|
+
return self.role_manager.get_system_prompt(self.role)
|
326
348
|
|
327
349
|
def _build_messages(self, user_input: str) -> List[dict]:
|
328
|
-
"""Build
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
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
|
334
361
|
|
335
362
|
def _handle_llm_response(self, user_input: str) -> Optional[str]:
|
336
363
|
"""Get response from API (streaming or normal) and print it.
|
@@ -343,16 +370,16 @@ class CLI:
|
|
343
370
|
Optional[str]: The assistant's response content or None if an error occurred.
|
344
371
|
"""
|
345
372
|
messages = self._build_messages(user_input)
|
346
|
-
|
347
|
-
|
348
|
-
|
373
|
+
if self.verbose:
|
374
|
+
self.console.print(messages)
|
375
|
+
is_code_mode = self.role == DefaultRoleNames.CODER
|
349
376
|
try:
|
350
|
-
if self.config
|
377
|
+
if self.config["STREAM"]:
|
351
378
|
stream_iterator = self.api_client.stream_completion(messages)
|
352
|
-
content, reasoning = self.printer.display_stream(stream_iterator)
|
379
|
+
content, reasoning = self.printer.display_stream(stream_iterator, not is_code_mode)
|
353
380
|
else:
|
354
381
|
content, reasoning = self.api_client.completion(messages)
|
355
|
-
self.printer.display_normal(content, reasoning)
|
382
|
+
self.printer.display_normal(content, reasoning, not is_code_mode)
|
356
383
|
|
357
384
|
if content is not None:
|
358
385
|
# Add only the content (not reasoning) to history
|
@@ -382,6 +409,7 @@ class CLI:
|
|
382
409
|
self._confirm_and_execute(content)
|
383
410
|
return True
|
384
411
|
|
412
|
+
# ------------------- REPL Methods -------------------
|
385
413
|
def _print_welcome_message(self) -> None:
|
386
414
|
"""Prints the initial welcome banner and instructions."""
|
387
415
|
self.console.print(
|
@@ -446,21 +474,6 @@ class CLI:
|
|
446
474
|
|
447
475
|
self.console.print("\nExiting YAICLI... Goodbye!", style="bold green")
|
448
476
|
|
449
|
-
def _run_once(self, prompt_arg: str, is_shell_mode: bool) -> None:
|
450
|
-
"""Run a single command (non-interactive)."""
|
451
|
-
self.current_mode = EXEC_MODE if is_shell_mode else TEMP_MODE
|
452
|
-
if not self.config.get("API_KEY"):
|
453
|
-
self.console.print("[bold red]Error:[/bold red] API key not found.")
|
454
|
-
raise typer.Exit(code=1)
|
455
|
-
|
456
|
-
content = self._handle_llm_response(prompt_arg)
|
457
|
-
|
458
|
-
if content is None:
|
459
|
-
raise typer.Exit(code=1)
|
460
|
-
|
461
|
-
if is_shell_mode:
|
462
|
-
self._confirm_and_execute(content)
|
463
|
-
|
464
477
|
def prepare_chat_loop(self) -> None:
|
465
478
|
"""Setup key bindings and history for interactive modes."""
|
466
479
|
self._setup_key_bindings()
|
@@ -477,6 +490,7 @@ class CLI:
|
|
477
490
|
self.session = PromptSession(key_bindings=self.bindings)
|
478
491
|
if self.chat_title:
|
479
492
|
chat_info = self.chat_manager.load_chat_by_title(self.chat_title)
|
493
|
+
self.is_temp_session = False
|
480
494
|
self.history = chat_info.get("history", [])
|
481
495
|
|
482
496
|
def _setup_key_bindings(self) -> None:
|
@@ -486,29 +500,48 @@ class CLI:
|
|
486
500
|
def _(event: KeyPressEvent) -> None:
|
487
501
|
self.current_mode = EXEC_MODE if self.current_mode == CHAT_MODE else CHAT_MODE
|
488
502
|
|
489
|
-
def
|
490
|
-
"""
|
491
|
-
if
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
self.current_mode = CHAT_MODE
|
503
|
+
def _run_once(self, input: str, shell: bool) -> None:
|
504
|
+
"""Run a single command (non-interactive)."""
|
505
|
+
self.current_mode = EXEC_MODE if shell else TEMP_MODE
|
506
|
+
if not self.config.get("API_KEY"):
|
507
|
+
self.console.print("[bold red]Error:[/bold red] API key not found.")
|
508
|
+
raise typer.Exit(code=1)
|
496
509
|
|
497
|
-
|
498
|
-
if prompt:
|
499
|
-
self.chat_start_time = int(time.time())
|
500
|
-
self.chat_title = self.chat_manager.make_chat_title(prompt)
|
501
|
-
self.console.print(f"Chat title: [bold]{self.chat_title}[/bold]")
|
502
|
-
self.is_temp_session = False
|
503
|
-
else:
|
504
|
-
# Mark as temporary session if no prompt
|
505
|
-
self.is_temp_session = True
|
506
|
-
self.console.print(
|
507
|
-
"Starting a temporary chat session (will not be saved automatically)", style="yellow"
|
508
|
-
)
|
510
|
+
content = self._handle_llm_response(input)
|
509
511
|
|
512
|
+
if content is None:
|
513
|
+
raise typer.Exit(code=1)
|
514
|
+
|
515
|
+
if shell:
|
516
|
+
self._confirm_and_execute(content)
|
517
|
+
|
518
|
+
# ------------------- Main Entry Point -------------------
|
519
|
+
def run(
|
520
|
+
self,
|
521
|
+
chat: bool,
|
522
|
+
shell: bool,
|
523
|
+
input: Optional[str],
|
524
|
+
role: Optional[str | Literal[DefaultRoleNames.DEFAULT]] = None,
|
525
|
+
) -> None:
|
526
|
+
"""Run the CLI in the appropriate mode with the selected role."""
|
527
|
+
self.set_role(role or self.role)
|
528
|
+
|
529
|
+
# Now handle normal operation
|
530
|
+
if shell:
|
531
|
+
# Set mode to shell
|
532
|
+
self.role = DefaultRoleNames.SHELL
|
533
|
+
if input:
|
534
|
+
self._run_once(input, shell=True)
|
535
|
+
else:
|
536
|
+
self.console.print("No prompt provided for shell mode.", style="yellow")
|
537
|
+
elif chat:
|
538
|
+
# Start interactive chat mode
|
539
|
+
self.current_mode = CHAT_MODE
|
540
|
+
self.chat_title = input if input else None
|
541
|
+
self.prepare_chat_loop()
|
510
542
|
self._run_repl()
|
511
|
-
elif
|
512
|
-
|
543
|
+
elif input:
|
544
|
+
# Run once with the given prompt
|
545
|
+
self._run_once(input, shell=False)
|
513
546
|
else:
|
514
547
|
self.console.print("No chat or prompt provided. Exiting.")
|
yaicli/config.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import configparser
|
2
|
+
from functools import lru_cache
|
2
3
|
from os import getenv
|
3
4
|
from typing import Optional
|
4
5
|
|
@@ -10,6 +11,7 @@ from yaicli.const import (
|
|
10
11
|
DEFAULT_CHAT_HISTORY_DIR,
|
11
12
|
DEFAULT_CONFIG_INI,
|
12
13
|
DEFAULT_CONFIG_MAP,
|
14
|
+
DEFAULT_JUSTIFY,
|
13
15
|
DEFAULT_MAX_SAVED_CHATS,
|
14
16
|
)
|
15
17
|
from yaicli.utils import str2bool
|
@@ -36,6 +38,7 @@ class Config(dict):
|
|
36
38
|
def __init__(self, console: Optional[Console] = None):
|
37
39
|
"""Initializes and loads the configuration."""
|
38
40
|
self.console = console or get_console()
|
41
|
+
|
39
42
|
super().__init__()
|
40
43
|
self.reload()
|
41
44
|
|
@@ -73,6 +76,8 @@ class Config(dict):
|
|
73
76
|
f.write(f"\nCHAT_HISTORY_DIR={DEFAULT_CHAT_HISTORY_DIR}\n")
|
74
77
|
if "MAX_SAVED_CHATS" not in config_content.strip(): # Check for empty lines
|
75
78
|
f.write(f"\nMAX_SAVED_CHATS={DEFAULT_MAX_SAVED_CHATS}\n")
|
79
|
+
if "JUSTIFY" not in config_content.strip():
|
80
|
+
f.write(f"\nJUSTIFY={DEFAULT_JUSTIFY}\n")
|
76
81
|
|
77
82
|
def _load_from_file(self) -> None:
|
78
83
|
"""Load configuration from the config file.
|
@@ -80,7 +85,7 @@ class Config(dict):
|
|
80
85
|
Creates default config file if it doesn't exist.
|
81
86
|
"""
|
82
87
|
if not CONFIG_PATH.exists():
|
83
|
-
self.console.print("Creating default configuration file.", style="bold yellow")
|
88
|
+
self.console.print("Creating default configuration file.", style="bold yellow", justify=self.get("JUSTIFY"))
|
84
89
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
85
90
|
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
86
91
|
f.write(DEFAULT_CONFIG_INI)
|
@@ -135,6 +140,7 @@ class Config(dict):
|
|
135
140
|
f"[yellow]Warning:[/yellow] Invalid value '{raw_value}' for '{key}'. "
|
136
141
|
f"Expected type '{target_type.__name__}'. Using default value '{default_values_str[key]}'. Error: {e}",
|
137
142
|
style="dim",
|
143
|
+
justify=self.get("JUSTIFY"),
|
138
144
|
)
|
139
145
|
# Fallback to default string value if conversion fails
|
140
146
|
try:
|
@@ -147,7 +153,17 @@ class Config(dict):
|
|
147
153
|
self.console.print(
|
148
154
|
f"[red]Error:[/red] Could not convert default value for '{key}'. Using raw value.",
|
149
155
|
style="error",
|
156
|
+
justify=self.get("JUSTIFY"),
|
150
157
|
)
|
151
158
|
converted_value = raw_value # Or assign a hardcoded safe default
|
152
159
|
|
153
160
|
self[key] = converted_value
|
161
|
+
|
162
|
+
|
163
|
+
@lru_cache(maxsize=1)
|
164
|
+
def get_config() -> Config:
|
165
|
+
"""Get the configuration singleton"""
|
166
|
+
return Config()
|
167
|
+
|
168
|
+
|
169
|
+
cfg = get_config()
|