yaicli 0.0.19__py3-none-any.whl → 0.1.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.
yaicli/cli.py ADDED
@@ -0,0 +1,332 @@
1
+ import subprocess
2
+ import sys
3
+ import traceback
4
+ from os.path import devnull
5
+ from pathlib import Path
6
+ from typing import List, Optional, Tuple
7
+
8
+ import typer
9
+ from prompt_toolkit import PromptSession, prompt
10
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
11
+ from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
12
+ from prompt_toolkit.keys import Keys
13
+ from rich import get_console
14
+ from rich.markdown import Markdown
15
+ from rich.padding import Padding
16
+ from rich.panel import Panel
17
+ from rich.prompt import Prompt
18
+
19
+ from yaicli.api import ApiClient
20
+ from yaicli.config import CONFIG_PATH, Config
21
+ from yaicli.const import (
22
+ CHAT_MODE,
23
+ CMD_CLEAR,
24
+ CMD_EXIT,
25
+ CMD_HISTORY,
26
+ CMD_MODE,
27
+ DEFAULT_CODE_THEME,
28
+ DEFAULT_INTERACTIVE_ROUND,
29
+ DEFAULT_OS_NAME,
30
+ DEFAULT_PROMPT,
31
+ DEFAULT_SHELL_NAME,
32
+ EXEC_MODE,
33
+ SHELL_PROMPT,
34
+ TEMP_MODE,
35
+ )
36
+ from yaicli.history import LimitedFileHistory
37
+ from yaicli.printer import Printer
38
+ from yaicli.utils import detect_os, detect_shell, filter_command
39
+
40
+
41
+ class CLI:
42
+ HISTORY_FILE = Path("~/.yaicli_history").expanduser()
43
+
44
+ def __init__(
45
+ self, verbose: bool = False, api_client: Optional[ApiClient] = None, printer: Optional[Printer] = None
46
+ ):
47
+ self.verbose = verbose
48
+ self.console = get_console()
49
+ self.bindings = KeyBindings()
50
+ self.config: Config = Config(self.console)
51
+ self.current_mode: str = TEMP_MODE
52
+ self.history = []
53
+ self.interactive_max_history = self.config.get("INTERACTIVE_MAX_HISTORY", DEFAULT_INTERACTIVE_ROUND)
54
+
55
+ # Detect OS and Shell if set to auto
56
+ if self.config.get("OS_NAME") == DEFAULT_OS_NAME:
57
+ self.config["OS_NAME"] = detect_os(self.config)
58
+ if self.config.get("SHELL_NAME") == DEFAULT_SHELL_NAME:
59
+ self.config["SHELL_NAME"] = detect_shell(self.config)
60
+
61
+ if self.verbose:
62
+ self.console.print("Loading Configuration:", style="bold cyan")
63
+ self.console.print(f"Config file path: {CONFIG_PATH}")
64
+ for key, value in self.config.items():
65
+ display_value = "****" if key == "API_KEY" and value else value
66
+ self.console.print(f" {key:<16}: {display_value}")
67
+ self.console.print(Markdown("---", code_theme=self.config.get("CODE_THEME", DEFAULT_CODE_THEME)))
68
+
69
+ self.api_client = api_client or ApiClient(self.config, self.console, self.verbose)
70
+ self.printer = printer or Printer(self.config, self.console, self.verbose)
71
+
72
+ _origin_stderr = None
73
+ if not sys.stdin.isatty():
74
+ _origin_stderr = sys.stderr
75
+ sys.stderr = open(devnull, "w", encoding="utf-8")
76
+ try:
77
+ self.session = PromptSession(key_bindings=self.bindings)
78
+ finally:
79
+ if _origin_stderr:
80
+ sys.stderr.close()
81
+ sys.stderr = _origin_stderr
82
+
83
+ def get_prompt_tokens(self) -> List[Tuple[str, str]]:
84
+ """Return prompt tokens for current mode"""
85
+ qmark = "💬" if self.current_mode == CHAT_MODE else "🚀" if self.current_mode == EXEC_MODE else "📝"
86
+ return [("class:qmark", f" {qmark} "), ("class:prompt", "> ")]
87
+
88
+ def _check_history_len(self) -> None:
89
+ """Check history length and remove oldest messages if necessary"""
90
+ target_len = self.interactive_max_history * 2
91
+ if len(self.history) > target_len:
92
+ self.history = self.history[-target_len:]
93
+
94
+ def _handle_special_commands(self, user_input: str) -> Optional[bool]:
95
+ """Handle special command return: True-continue loop, False-exit loop, None-non-special command"""
96
+ command = user_input.lower().strip()
97
+ if command == CMD_EXIT:
98
+ return False
99
+ if command == CMD_CLEAR and self.current_mode == CHAT_MODE:
100
+ self.history.clear()
101
+ self.console.print("Chat history cleared", style="bold yellow")
102
+ return True
103
+ if command == CMD_HISTORY:
104
+ if not self.history:
105
+ self.console.print("History is empty.", style="yellow")
106
+ else:
107
+ self.console.print("Chat History:", style="bold underline")
108
+ code_theme = self.config.get("CODE_THEME", "monokai")
109
+ for i in range(0, len(self.history), 2):
110
+ user_msg = self.history[i]
111
+ assistant_msg = self.history[i + 1] if (i + 1) < len(self.history) else None
112
+ self.console.print(f"[dim]{i // 2 + 1}[/dim] [bold blue]User:[/bold blue] {user_msg['content']}")
113
+ if assistant_msg:
114
+ md = Markdown(assistant_msg["content"], code_theme=code_theme)
115
+ padded_md = Padding(md, (0, 0, 0, 4))
116
+ self.console.print(" Assistant:", style="bold green")
117
+ self.console.print(padded_md)
118
+ return True
119
+ # Handle /mode command
120
+ if command.startswith(CMD_MODE):
121
+ parts = command.split(maxsplit=1)
122
+ if len(parts) == 2 and parts[1] in [CHAT_MODE, EXEC_MODE]:
123
+ new_mode = parts[1]
124
+ if self.current_mode != new_mode:
125
+ self.current_mode = new_mode
126
+ mode_name = "Chat" if self.current_mode == CHAT_MODE else "Exec"
127
+ self.console.print(f"Switched to [bold yellow]{mode_name}[/bold yellow] mode")
128
+ else:
129
+ self.console.print(f"Already in {self.current_mode} mode.", style="yellow")
130
+ else:
131
+ self.console.print(f"Usage: {CMD_MODE} {CHAT_MODE}|{EXEC_MODE}", style="yellow")
132
+ return True
133
+ return None
134
+
135
+ def _confirm_and_execute(self, raw_content: str) -> None:
136
+ """Review, edit and execute the command"""
137
+ cmd = filter_command(raw_content)
138
+ if not cmd:
139
+ self.console.print("No command generated or command is empty.", style="bold red")
140
+ return
141
+ self.console.print(
142
+ Panel(cmd, title="Suggest Command", title_align="left", border_style="bold magenta", expand=False)
143
+ )
144
+ _input = Prompt.ask(
145
+ r"Execute command? \[e]dit, \[y]es, \[n]o",
146
+ choices=["y", "n", "e"],
147
+ default="n",
148
+ case_sensitive=False,
149
+ show_choices=False,
150
+ )
151
+ executed_cmd = None
152
+ if _input == "y":
153
+ executed_cmd = cmd
154
+ elif _input == "e":
155
+ try:
156
+ edited_cmd = prompt("Edit command: ", default=cmd).strip()
157
+ if edited_cmd and edited_cmd != cmd:
158
+ executed_cmd = edited_cmd
159
+ elif edited_cmd:
160
+ executed_cmd = cmd
161
+ else:
162
+ self.console.print("Execution cancelled.", style="yellow")
163
+ except EOFError:
164
+ self.console.print("\nEdit cancelled.", style="yellow")
165
+ return
166
+ if executed_cmd:
167
+ self.console.print("--- Executing --- ", style="bold green")
168
+ try:
169
+ subprocess.call(executed_cmd, shell=True)
170
+ except Exception as e:
171
+ self.console.print(f"[red]Failed to execute command: {e}[/red]")
172
+ self.console.print("--- Finished ---", style="bold green")
173
+ elif _input != "e":
174
+ self.console.print("Execution cancelled.", style="yellow")
175
+
176
+ def get_system_prompt(self) -> str:
177
+ """Return system prompt for current mode"""
178
+ prompt_template = SHELL_PROMPT if self.current_mode == EXEC_MODE else DEFAULT_PROMPT
179
+ return prompt_template.format(
180
+ _os=self.config.get("OS_NAME", "Unknown OS"), _shell=self.config.get("SHELL_NAME", "Unknown Shell")
181
+ )
182
+
183
+ def _build_messages(self, user_input: str) -> List[dict]:
184
+ """Build the list of messages for the API call."""
185
+ return [
186
+ {"role": "system", "content": self.get_system_prompt()},
187
+ *self.history,
188
+ {"role": "user", "content": user_input},
189
+ ]
190
+
191
+ def _handle_llm_response(self, user_input: str) -> Optional[str]:
192
+ """Get response from API (streaming or normal) and print it.
193
+ Returns the full content string or None if an error occurred.
194
+
195
+ Args:
196
+ user_input (str): The user's input text.
197
+
198
+ Returns:
199
+ Optional[str]: The assistant's response content or None if an error occurred.
200
+ """
201
+ messages = self._build_messages(user_input)
202
+ content = None
203
+ reasoning = None
204
+
205
+ try:
206
+ if self.config.get("STREAM", True):
207
+ stream_iterator = self.api_client.stream_completion(messages)
208
+ content, reasoning = self.printer.display_stream(stream_iterator)
209
+ else:
210
+ content, reasoning = self.api_client.completion(messages)
211
+ self.printer.display_normal(content, reasoning)
212
+
213
+ if content is not None:
214
+ # Add only the content (not reasoning) to history
215
+ self.history.extend(
216
+ [{"role": "user", "content": user_input}, {"role": "assistant", "content": content}]
217
+ )
218
+ self._check_history_len()
219
+ return content
220
+ else:
221
+ return None
222
+ except Exception as e:
223
+ self.console.print(f"[red]Error processing LLM response: {e}[/red]")
224
+ if self.verbose:
225
+ traceback.print_exc()
226
+ return None
227
+
228
+ def _process_user_input(self, user_input: str) -> bool:
229
+ """Process user input: get response, print, update history, maybe execute.
230
+ Returns True to continue REPL, False to exit on critical error.
231
+ """
232
+ content = self._handle_llm_response(user_input)
233
+
234
+ if content is None:
235
+ return True
236
+
237
+ if self.current_mode == EXEC_MODE:
238
+ self._confirm_and_execute(content)
239
+ return True
240
+
241
+ def _print_welcome_message(self) -> None:
242
+ """Prints the initial welcome banner and instructions."""
243
+ self.console.print(
244
+ """
245
+ ██ ██ █████ ██ ██████ ██ ██
246
+ ██ ██ ██ ██ ██ ██ ██ ██
247
+ ████ ███████ ██ ██ ██ ██
248
+ ██ ██ ██ ██ ██ ██ ██
249
+ ██ ██ ██ ██ ██████ ███████ ██
250
+ """,
251
+ style="bold cyan",
252
+ )
253
+ self.console.print("Welcome to YAICLI!", style="bold")
254
+ self.console.print("Press [bold yellow]TAB[/bold yellow] to switch mode")
255
+ self.console.print(f"{CMD_CLEAR:<19}: Clear chat history")
256
+ self.console.print(f"{CMD_HISTORY:<19}: Show chat history")
257
+ cmd_exit = f"{CMD_EXIT}|Ctrl+D|Ctrl+C"
258
+ self.console.print(f"{cmd_exit:<19}: Exit")
259
+ cmd_mode = f"{CMD_MODE} {CHAT_MODE}|{EXEC_MODE}"
260
+ self.console.print(f"{cmd_mode:<19}: Switch mode (Case insensitive)", style="dim")
261
+
262
+ def _run_repl(self) -> None:
263
+ """Run the main Read-Eval-Print Loop (REPL)."""
264
+ self.prepare_chat_loop()
265
+ self._print_welcome_message()
266
+ while True:
267
+ self.console.print(Markdown("---", code_theme=self.config.get("CODE_THEME", "monokai")))
268
+ try:
269
+ user_input = self.session.prompt(self.get_prompt_tokens)
270
+ user_input = user_input.strip()
271
+ if not user_input:
272
+ continue
273
+ command_result = self._handle_special_commands(user_input)
274
+ if command_result is False:
275
+ break
276
+ if command_result is True:
277
+ continue
278
+ if not self._process_user_input(user_input):
279
+ break
280
+ except (KeyboardInterrupt, EOFError):
281
+ break
282
+ self.console.print("\nExiting YAICLI... Goodbye!", style="bold green")
283
+
284
+ def _run_once(self, prompt_arg: str, is_shell_mode: bool) -> None:
285
+ """Run a single command (non-interactive)."""
286
+ self.current_mode = EXEC_MODE if is_shell_mode else TEMP_MODE
287
+ if not self.config.get("API_KEY"):
288
+ self.console.print("[bold red]Error:[/bold red] API key not found.")
289
+ raise typer.Exit(code=1)
290
+
291
+ content = self._handle_llm_response(prompt_arg)
292
+
293
+ if content is None:
294
+ raise typer.Exit(code=1)
295
+
296
+ if is_shell_mode:
297
+ self._confirm_and_execute(content)
298
+
299
+ def prepare_chat_loop(self) -> None:
300
+ """Setup key bindings and history for interactive modes."""
301
+ self._setup_key_bindings()
302
+ self.HISTORY_FILE.touch(exist_ok=True)
303
+ try:
304
+ self.session = PromptSession(
305
+ key_bindings=self.bindings,
306
+ history=LimitedFileHistory(self.HISTORY_FILE, max_entries=self.interactive_max_history),
307
+ auto_suggest=AutoSuggestFromHistory() if self.config.get("AUTO_SUGGEST", True) else None,
308
+ enable_history_search=True,
309
+ )
310
+ except Exception as e:
311
+ self.console.print(f"[red]Error initializing prompt session history: {e}[/red]")
312
+ self.session = PromptSession(key_bindings=self.bindings)
313
+
314
+ def _setup_key_bindings(self) -> None:
315
+ """Setup keyboard shortcuts (e.g., TAB for mode switching)."""
316
+
317
+ @self.bindings.add(Keys.ControlI) # TAB
318
+ def _(event: KeyPressEvent) -> None:
319
+ self.current_mode = EXEC_MODE if self.current_mode == CHAT_MODE else CHAT_MODE
320
+
321
+ def run(self, chat: bool, shell: bool, prompt: Optional[str]) -> None:
322
+ """Main entry point to run the CLI (REPL or single command)."""
323
+ if chat:
324
+ if not self.config.get("API_KEY"):
325
+ self.console.print("[bold red]Error:[/bold red] API key not found. Cannot start chat mode.")
326
+ return
327
+ self.current_mode = CHAT_MODE
328
+ self._run_repl()
329
+ elif prompt:
330
+ self._run_once(prompt, shell)
331
+ else:
332
+ self.console.print("No chat or prompt provided. Exiting.")
yaicli/config.py ADDED
@@ -0,0 +1,183 @@
1
+ import configparser
2
+ from os import getenv
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from rich import get_console
7
+ from rich.console import Console
8
+
9
+ from yaicli.utils import str2bool
10
+
11
+ DEFAULT_CONFIG_MAP = {
12
+ # Core API settings
13
+ "BASE_URL": {"value": "https://api.openai.com/v1", "env_key": "YAI_BASE_URL", "type": str},
14
+ "API_KEY": {"value": "", "env_key": "YAI_API_KEY", "type": str},
15
+ "MODEL": {"value": "gpt-4o", "env_key": "YAI_MODEL", "type": str},
16
+ # System detection hints
17
+ "SHELL_NAME": {"value": "auto", "env_key": "YAI_SHELL_NAME", "type": str},
18
+ "OS_NAME": {"value": "auto", "env_key": "YAI_OS_NAME", "type": str},
19
+ # API response parsing
20
+ "COMPLETION_PATH": {"value": "chat/completions", "env_key": "YAI_COMPLETION_PATH", "type": str},
21
+ "ANSWER_PATH": {"value": "choices[0].message.content", "env_key": "YAI_ANSWER_PATH", "type": str},
22
+ # API call parameters
23
+ "STREAM": {"value": "true", "env_key": "YAI_STREAM", "type": bool},
24
+ "TEMPERATURE": {"value": "0.7", "env_key": "YAI_TEMPERATURE", "type": float},
25
+ "TOP_P": {"value": "1.0", "env_key": "YAI_TOP_P", "type": float},
26
+ "MAX_TOKENS": {"value": "1024", "env_key": "YAI_MAX_TOKENS", "type": int},
27
+ # UI/UX settings
28
+ "CODE_THEME": {"value": "monokai", "env_key": "YAI_CODE_THEME", "type": str},
29
+ "MAX_HISTORY": {"value": "500", "env_key": "YAI_MAX_HISTORY", "type": int},
30
+ "AUTO_SUGGEST": {"value": "true", "env_key": "YAI_AUTO_SUGGEST", "type": bool},
31
+ }
32
+
33
+ DEFAULT_CONFIG_INI = f"""[core]
34
+ PROVIDER=openai
35
+ BASE_URL={DEFAULT_CONFIG_MAP["BASE_URL"]["value"]}
36
+ API_KEY={DEFAULT_CONFIG_MAP["API_KEY"]["value"]}
37
+ MODEL={DEFAULT_CONFIG_MAP["MODEL"]["value"]}
38
+
39
+ # auto detect shell and os (or specify manually, e.g., bash, zsh, powershell.exe)
40
+ SHELL_NAME={DEFAULT_CONFIG_MAP["SHELL_NAME"]["value"]}
41
+ OS_NAME={DEFAULT_CONFIG_MAP["OS_NAME"]["value"]}
42
+
43
+ # API paths (usually no need to change for OpenAI compatible APIs)
44
+ COMPLETION_PATH={DEFAULT_CONFIG_MAP["COMPLETION_PATH"]["value"]}
45
+ ANSWER_PATH={DEFAULT_CONFIG_MAP["ANSWER_PATH"]["value"]}
46
+
47
+ # true: streaming response, false: non-streaming
48
+ STREAM={DEFAULT_CONFIG_MAP["STREAM"]["value"]}
49
+
50
+ # LLM parameters
51
+ TEMPERATURE={DEFAULT_CONFIG_MAP["TEMPERATURE"]["value"]}
52
+ TOP_P={DEFAULT_CONFIG_MAP["TOP_P"]["value"]}
53
+ MAX_TOKENS={DEFAULT_CONFIG_MAP["MAX_TOKENS"]["value"]}
54
+
55
+ # UI/UX
56
+ CODE_THEME={DEFAULT_CONFIG_MAP["CODE_THEME"]["value"]}
57
+ MAX_HISTORY={DEFAULT_CONFIG_MAP["MAX_HISTORY"]["value"]}
58
+ AUTO_SUGGEST={DEFAULT_CONFIG_MAP["AUTO_SUGGEST"]["value"]}
59
+ """
60
+
61
+ CONFIG_PATH = Path("~/.config/yaicli/config.ini").expanduser()
62
+
63
+
64
+ class CasePreservingConfigParser(configparser.RawConfigParser):
65
+ """Case preserving config parser"""
66
+
67
+ def optionxform(self, optionstr):
68
+ return optionstr
69
+
70
+
71
+ class Config(dict):
72
+ """Configuration class that loads settings on initialization.
73
+
74
+ This class encapsulates the configuration loading logic with priority:
75
+ 1. Environment variables (highest priority)
76
+ 2. Configuration file
77
+ 3. Default values (lowest priority)
78
+
79
+ It handles type conversion and validation based on DEFAULT_CONFIG_MAP.
80
+ """
81
+
82
+ def __init__(self, console: Optional[Console] = None):
83
+ """Initializes and loads the configuration."""
84
+ self.console = console or get_console()
85
+ super().__init__()
86
+ self.reload()
87
+
88
+ def reload(self) -> None:
89
+ """Reload configuration from all sources.
90
+
91
+ Follows priority order: env vars > config file > defaults
92
+ """
93
+ # Start with defaults
94
+ self.clear()
95
+ self.update(self._load_defaults())
96
+
97
+ # Load from config file
98
+ file_config = self._load_from_file()
99
+ if file_config:
100
+ self.update(file_config)
101
+
102
+ # Load from environment variables and apply type conversion
103
+ self._load_from_env()
104
+ self._apply_type_conversion()
105
+
106
+ def _load_defaults(self) -> dict[str, str]:
107
+ """Load default configuration values as strings.
108
+
109
+ Returns:
110
+ Dictionary with default configuration values
111
+ """
112
+ return {k: v["value"] for k, v in DEFAULT_CONFIG_MAP.items()}
113
+
114
+ def _load_from_file(self) -> dict[str, str]:
115
+ """Load configuration from the config file.
116
+
117
+ Creates default config file if it doesn't exist.
118
+
119
+ Returns:
120
+ Dictionary with configuration values from file, or empty dict if no valid values
121
+ """
122
+ if not CONFIG_PATH.exists():
123
+ self.console.print("Creating default configuration file.", style="bold yellow")
124
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
125
+ with open(CONFIG_PATH, "w", encoding="utf-8") as f:
126
+ f.write(DEFAULT_CONFIG_INI)
127
+ return {}
128
+
129
+ config_parser = CasePreservingConfigParser()
130
+ config_parser.read(CONFIG_PATH, encoding="utf-8")
131
+ if "core" in config_parser:
132
+ return {k: v for k, v in config_parser["core"].items() if k in DEFAULT_CONFIG_MAP and v.strip()}
133
+ return {}
134
+
135
+ def _load_from_env(self) -> None:
136
+ """Load configuration from environment variables.
137
+
138
+ Updates the configuration dictionary in-place.
139
+ """
140
+ for key, config_info in DEFAULT_CONFIG_MAP.items():
141
+ env_value = getenv(config_info["env_key"])
142
+ if env_value is not None:
143
+ self[key] = env_value
144
+
145
+ def _apply_type_conversion(self) -> None:
146
+ """Apply type conversion to configuration values.
147
+
148
+ Updates the configuration dictionary in-place with properly typed values.
149
+ Falls back to default values if conversion fails.
150
+ """
151
+ default_values_str = self._load_defaults()
152
+
153
+ for key, config_info in DEFAULT_CONFIG_MAP.items():
154
+ target_type = config_info["type"]
155
+ raw_value = self.get(key, default_values_str.get(key))
156
+ converted_value = None
157
+
158
+ try:
159
+ if target_type is bool:
160
+ converted_value = str2bool(raw_value)
161
+ elif target_type in (int, float, str):
162
+ converted_value = target_type(raw_value)
163
+ except (ValueError, TypeError) as e:
164
+ self.console.print(
165
+ f"[yellow]Warning:[/yellow] Invalid value '{raw_value}' for '{key}'. "
166
+ f"Expected type '{target_type.__name__}'. Using default value '{default_values_str[key]}'. Error: {e}",
167
+ style="dim",
168
+ )
169
+ # Fallback to default string value if conversion fails
170
+ try:
171
+ if target_type is bool:
172
+ converted_value = str2bool(default_values_str[key])
173
+ else:
174
+ converted_value = target_type(default_values_str[key])
175
+ except (ValueError, TypeError):
176
+ # If default also fails (unlikely), keep the raw merged value or a sensible default
177
+ self.console.print(
178
+ f"[red]Error:[/red] Could not convert default value for '{key}'. Using raw value.",
179
+ style="error",
180
+ )
181
+ converted_value = raw_value # Or assign a hardcoded safe default
182
+
183
+ self[key] = converted_value
yaicli/const.py ADDED
@@ -0,0 +1,119 @@
1
+ from enum import StrEnum
2
+
3
+ CMD_CLEAR = "/clear"
4
+ CMD_EXIT = "/exit"
5
+ CMD_HISTORY = "/his"
6
+ CMD_MODE = "/mode"
7
+
8
+ EXEC_MODE = "exec"
9
+ CHAT_MODE = "chat"
10
+ TEMP_MODE = "temp"
11
+
12
+ DEFAULT_CONFIG_PATH = "~/.config/yaicli/config.ini"
13
+ DEFAULT_CODE_THEME = "monokai"
14
+ DEFAULT_COMPLETION_PATH = "chat/completions"
15
+ DEFAULT_ANSWER_PATH = "choices[0].message.content"
16
+ DEFAULT_PROVIDER = "openai"
17
+ DEFAULT_BASE_URL = "https://api.openai.com/v1"
18
+ DEFAULT_MODEL = "gpt-4o"
19
+ DEFAULT_SHELL_NAME = "auto"
20
+ DEFAULT_OS_NAME = "auto"
21
+ DEFAULT_STREAM = "true"
22
+ DEFAULT_TEMPERATURE: float = 0.7
23
+ DEFAULT_TOP_P: float = 1.0
24
+ DEFAULT_MAX_TOKENS: int = 1024
25
+ DEFAULT_MAX_HISTORY: int = 500
26
+ DEFAULT_AUTO_SUGGEST = "true"
27
+ DEFAULT_TIMEOUT: int = 60
28
+ DEFAULT_INTERACTIVE_ROUND: int = 25
29
+
30
+
31
+ class EventTypeEnum(StrEnum):
32
+ """Enumeration of possible event types from the SSE stream."""
33
+
34
+ ERROR = "error"
35
+ REASONING = "reasoning"
36
+ REASONING_END = "reasoning_end"
37
+ CONTENT = "content"
38
+ FINISH = "finish"
39
+
40
+
41
+ SHELL_PROMPT = """Your are a Shell Command Generator.
42
+ Generate a command EXCLUSIVELY for {_os} OS with {_shell} shell.
43
+ If details are missing, offer the most logical solution.
44
+ Ensure the output is a valid shell command.
45
+ Combine multiple steps with `&&` when possible.
46
+ Supply plain text only, avoiding Markdown formatting."""
47
+
48
+ DEFAULT_PROMPT = (
49
+ "You are YAICLI, a system management and programing assistant, "
50
+ "You are managing {_os} operating system with {_shell} shell. "
51
+ "Your responses should be concise and use Markdown format (but dont't use ```markdown), "
52
+ "unless the user explicitly requests more details."
53
+ )
54
+
55
+ # DEFAULT_CONFIG_MAP is a dictionary of the configuration options.
56
+ # The key is the name of the configuration option.
57
+ # The value is a dictionary with the following keys:
58
+ # - value: the default value of the configuration option
59
+ # - env_key: the environment variable key of the configuration option
60
+ # - type: the type of the configuration option
61
+ DEFAULT_CONFIG_MAP = {
62
+ # Core API settings
63
+ "BASE_URL": {"value": DEFAULT_BASE_URL, "env_key": "YAI_BASE_URL", "type": str},
64
+ "API_KEY": {"value": "", "env_key": "YAI_API_KEY", "type": str},
65
+ "MODEL": {"value": DEFAULT_MODEL, "env_key": "YAI_MODEL", "type": str},
66
+ # System detection hints
67
+ "SHELL_NAME": {"value": DEFAULT_SHELL_NAME, "env_key": "YAI_SHELL_NAME", "type": str},
68
+ "OS_NAME": {"value": DEFAULT_OS_NAME, "env_key": "YAI_OS_NAME", "type": str},
69
+ # API response parsing
70
+ "COMPLETION_PATH": {"value": DEFAULT_COMPLETION_PATH, "env_key": "YAI_COMPLETION_PATH", "type": str},
71
+ "ANSWER_PATH": {"value": DEFAULT_ANSWER_PATH, "env_key": "YAI_ANSWER_PATH", "type": str},
72
+ # API call parameters
73
+ "STREAM": {"value": DEFAULT_STREAM, "env_key": "YAI_STREAM", "type": bool},
74
+ "TEMPERATURE": {"value": DEFAULT_TEMPERATURE, "env_key": "YAI_TEMPERATURE", "type": float},
75
+ "TOP_P": {"value": DEFAULT_TOP_P, "env_key": "YAI_TOP_P", "type": float},
76
+ "MAX_TOKENS": {"value": DEFAULT_MAX_TOKENS, "env_key": "YAI_MAX_TOKENS", "type": int},
77
+ "TIMEOUT": {"value": DEFAULT_TIMEOUT, "env_key": "YAI_TIMEOUT", "type": int},
78
+ "INTERACTIVE_ROUND": {
79
+ "value": DEFAULT_INTERACTIVE_ROUND,
80
+ "env_key": "YAI_INTERACTIVE_ROUND",
81
+ "type": int,
82
+ },
83
+ # UI/UX settings
84
+ "CODE_THEME": {"value": DEFAULT_CODE_THEME, "env_key": "YAI_CODE_THEME", "type": str},
85
+ "MAX_HISTORY": {"value": DEFAULT_MAX_HISTORY, "env_key": "YAI_MAX_HISTORY", "type": int},
86
+ "AUTO_SUGGEST": {"value": DEFAULT_AUTO_SUGGEST, "env_key": "YAI_AUTO_SUGGEST", "type": bool},
87
+ }
88
+
89
+ DEFAULT_CONFIG_INI = f"""[core]
90
+ PROVIDER={DEFAULT_PROVIDER}
91
+ BASE_URL={DEFAULT_CONFIG_MAP["BASE_URL"]["value"]}
92
+ API_KEY={DEFAULT_CONFIG_MAP["API_KEY"]["value"]}
93
+ MODEL={DEFAULT_CONFIG_MAP["MODEL"]["value"]}
94
+
95
+ # auto detect shell and os (or specify manually, e.g., bash, zsh, powershell.exe)
96
+ SHELL_NAME={DEFAULT_CONFIG_MAP["SHELL_NAME"]["value"]}
97
+ OS_NAME={DEFAULT_CONFIG_MAP["OS_NAME"]["value"]}
98
+
99
+ # API paths (usually no need to change for OpenAI compatible APIs)
100
+ COMPLETION_PATH={DEFAULT_CONFIG_MAP["COMPLETION_PATH"]["value"]}
101
+ ANSWER_PATH={DEFAULT_CONFIG_MAP["ANSWER_PATH"]["value"]}
102
+
103
+ # true: streaming response, false: non-streaming
104
+ STREAM={DEFAULT_CONFIG_MAP["STREAM"]["value"]}
105
+
106
+ # LLM parameters
107
+ TEMPERATURE={DEFAULT_CONFIG_MAP["TEMPERATURE"]["value"]}
108
+ TOP_P={DEFAULT_CONFIG_MAP["TOP_P"]["value"]}
109
+ MAX_TOKENS={DEFAULT_CONFIG_MAP["MAX_TOKENS"]["value"]}
110
+ TIMEOUT={DEFAULT_CONFIG_MAP["TIMEOUT"]["value"]}
111
+
112
+ # Interactive mode parameters
113
+ INTERACTIVE_ROUND={DEFAULT_CONFIG_MAP["INTERACTIVE_ROUND"]["value"]}
114
+
115
+ # UI/UX
116
+ CODE_THEME={DEFAULT_CONFIG_MAP["CODE_THEME"]["value"]}
117
+ MAX_HISTORY={DEFAULT_CONFIG_MAP["MAX_HISTORY"]["value"]} # Max entries kept in history file
118
+ AUTO_SUGGEST={DEFAULT_CONFIG_MAP["AUTO_SUGGEST"]["value"]}
119
+ """