yaicli 0.0.18__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.
@@ -0,0 +1,15 @@
1
+ pyproject.toml,sha256=X_S3bMzlnrCQkNibgrjMRVHIne1eOhnmj4SI89qyQGM,1519
2
+ yaicli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ yaicli/api.py,sha256=gKqkRc-sGg5uKfg97mMErxrRoLvQH9nb62yn4pp_7K0,13925
4
+ yaicli/cli.py,sha256=oOnsoF3vbmeYdkNq3nB5z7M-U8LJFXuGORWTCRANiXI,14199
5
+ yaicli/config.py,sha256=GrMKPix07nYIldDAZM2KCBL31fLlu4XbxUe_otZ9uBk,7260
6
+ yaicli/const.py,sha256=UBUgFe8P0aIfdYZJ0HQnXstVY43HpwpNk2qL_Ca6b60,4778
7
+ yaicli/entry.py,sha256=SlmsfaxLI26qslsFbGBHQA7wG9hjNqr5-j0OoICmfXY,3138
8
+ yaicli/history.py,sha256=s-57X9FMsaQHF7XySq1gGH_jpd_cHHTYafYu2ECuG6M,2472
9
+ yaicli/printer.py,sha256=HdV-eiJan8VpNXUFV7nckkCVnGkBQ6GMXxKf-SzY4Qw,9966
10
+ yaicli/utils.py,sha256=dchhz1s6XCDTxCT_PdPbSx93fPYcD9FUByHde8xWKMs,4024
11
+ yaicli-0.1.0.dist-info/METADATA,sha256=h-lrbZhmN8N2O0KKAkYEkwtXs6fnDv8XDVBk_EJKLsc,27784
12
+ yaicli-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ yaicli-0.1.0.dist-info/entry_points.txt,sha256=iMhGm3btBaqrknQoF6WCg5sdx69ZyNSC73tRpCcbcLw,63
14
+ yaicli-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
15
+ yaicli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ ai = yaicli.entry:app
3
+ yai = yaicli.entry:app
@@ -1,7 +0,0 @@
1
- pyproject.toml,sha256=WJralVkvvAn7dGKIrF6YUkW8FIwvrR4PSTfI5FdI7vE,1452
2
- yaicli.py,sha256=mkCXCW9fgv_RsKlGcOZZeIr6faDovavIkW-LbUIOMGc,25611
3
- yaicli-0.0.18.dist-info/METADATA,sha256=PJhD-m48hPdWXtLYJdKr0GLjm8H6FMv7OsnFH-w6e1Q,29923
4
- yaicli-0.0.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
- yaicli-0.0.18.dist-info/entry_points.txt,sha256=gdduQwAuu_LeDqnDU81Fv3NPmD2tRQ1FffvolIP3S1Q,34
6
- yaicli-0.0.18.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
7
- yaicli-0.0.18.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- ai = yaicli:app
yaicli.py DELETED
@@ -1,640 +0,0 @@
1
- import configparser
2
- import json
3
- import platform
4
- import subprocess
5
- import sys
6
- import time
7
- from os import getenv
8
- from os.path import basename, exists, pathsep, devnull
9
- from pathlib import Path
10
- from typing import Annotated, Any, Dict, Optional, Union
11
-
12
- import httpx
13
- import jmespath
14
- import typer
15
- from distro import name as distro_name
16
- from prompt_toolkit import PromptSession, prompt
17
- from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
18
- from prompt_toolkit.history import FileHistory, _StrOrBytesPath
19
- from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
20
- from prompt_toolkit.keys import Keys
21
- from rich.console import Console
22
- from rich.live import Live
23
- from rich.markdown import Markdown
24
- from rich.panel import Panel
25
- from rich.prompt import Prompt
26
-
27
- SHELL_PROMPT = """Your are a Shell Command Generator.
28
- Generate a command EXCLUSIVELY for {_os} OS with {_shell} shell.
29
- Rules:
30
- 1. Use ONLY {_shell}-specific syntax and connectors (&&, ||, |, etc)
31
- 2. Output STRICTLY in plain text format
32
- 3. NEVER use markdown, code blocks or explanations
33
- 4. Chain multi-step commands in SINGLE LINE
34
- 5. Return NOTHING except the ready-to-run command"""
35
-
36
- DEFAULT_PROMPT = (
37
- "You are YAICLI, a system management and programing assistant, "
38
- "You are managing {_os} operating system with {_shell} shell. "
39
- "Your responses should be concise and use Markdown format, "
40
- "unless the user explicitly requests more details."
41
- )
42
-
43
- CMD_CLEAR = "/clear"
44
- CMD_EXIT = "/exit"
45
- CMD_HISTORY = "/his"
46
-
47
- EXEC_MODE = "exec"
48
- CHAT_MODE = "chat"
49
- TEMP_MODE = "temp"
50
-
51
- DEFAULT_CONFIG_MAP = {
52
- "BASE_URL": {"value": "https://api.openai.com/v1", "env_key": "YAI_BASE_URL"},
53
- "API_KEY": {"value": "", "env_key": "YAI_API_KEY"},
54
- "MODEL": {"value": "gpt-4o", "env_key": "YAI_MODEL"},
55
- "SHELL_NAME": {"value": "auto", "env_key": "YAI_SHELL_NAME"},
56
- "OS_NAME": {"value": "auto", "env_key": "YAI_OS_NAME"},
57
- "COMPLETION_PATH": {"value": "chat/completions", "env_key": "YAI_COMPLETION_PATH"},
58
- "ANSWER_PATH": {"value": "choices[0].message.content", "env_key": "YAI_ANSWER_PATH"},
59
- "STREAM": {"value": "true", "env_key": "YAI_STREAM"},
60
- "CODE_THEME": {"value": "monokia", "env_key": "YAI_CODE_THEME"},
61
- "TEMPERATURE": {"value": "0.7", "env_key": "YAI_TEMPERATURE"},
62
- "TOP_P": {"value": "1.0", "env_key": "YAI_TOP_P"},
63
- "MAX_TOKENS": {"value": "1024", "env_key": "YAI_MAX_TOKENS"},
64
- "MAX_HISTORY": {"value": "500", "env_key": "YAI_MAX_HISTORY"},
65
- "AUTO_SUGGEST": {"value": "true", "env_key": "YAI_AUTO_SUGGEST"},
66
- }
67
-
68
- DEFAULT_CONFIG_INI = """[core]
69
- PROVIDER=openai
70
- BASE_URL=https://api.openai.com/v1
71
- API_KEY=
72
- MODEL=gpt-4o
73
-
74
- # auto detect shell and os
75
- SHELL_NAME=auto
76
- OS_NAME=auto
77
-
78
- # if you want to use custom completions path, you can set it here
79
- COMPLETION_PATH=/chat/completions
80
- # if you want to use custom answer path, you can set it here
81
- ANSWER_PATH=choices[0].message.content
82
-
83
- # true: streaming response
84
- # false: non-streaming response
85
- STREAM=true
86
- CODE_THEME=monokia
87
-
88
- TEMPERATURE=0.7
89
- TOP_P=1.0
90
- MAX_TOKENS=1024
91
-
92
- MAX_HISTORY=500
93
- AUTO_SUGGEST=true"""
94
-
95
- app = typer.Typer(
96
- name="yaicli",
97
- context_settings={"help_option_names": ["-h", "--help"]},
98
- pretty_exceptions_enable=False,
99
- )
100
-
101
-
102
- class CasePreservingConfigParser(configparser.RawConfigParser):
103
- """Case preserving config parser"""
104
-
105
- def optionxform(self, optionstr):
106
- return optionstr
107
-
108
-
109
- class LimitedFileHistory(FileHistory):
110
- def __init__(self, filename: _StrOrBytesPath, max_entries: int = 500, trim_every: int = 2):
111
- """Limited file history
112
- Args:
113
- filename (str): path to history file
114
- max_entries (int): maximum number of entries to keep
115
- trim_every (int): trim history every `trim_every` appends
116
-
117
- Example:
118
- >>> history = LimitedFileHistory("~/.yaicli_history", max_entries=500, trim_every=10)
119
- >>> history.append_string("echo hello")
120
- >>> history.append_string("echo world")
121
- >>> session = PromptSession(history=history)
122
- """
123
- self.max_entries = max_entries
124
- self._append_count = 0
125
- self._trim_every = trim_every
126
- super().__init__(filename)
127
-
128
- def store_string(self, string: str) -> None:
129
- # Call the original method to deposit a new record
130
- super().store_string(string)
131
-
132
- self._append_count += 1
133
- if self._append_count >= self._trim_every:
134
- self._trim_history()
135
- self._append_count = 0
136
-
137
- def _trim_history(self):
138
- if not exists(self.filename):
139
- return
140
-
141
- with open(self.filename, "r", encoding="utf-8") as f:
142
- lines = f.readlines()
143
-
144
- # By record: each record starts with "# timestamp" followed by a number of "+lines".
145
- entries = []
146
- current_entry = []
147
-
148
- for line in lines:
149
- if line.startswith("# "):
150
- if current_entry:
151
- entries.append(current_entry)
152
- current_entry = [line]
153
- elif line.startswith("+") or line.strip() == "":
154
- current_entry.append(line)
155
-
156
- if current_entry:
157
- entries.append(current_entry)
158
-
159
- # Keep the most recent max_entries row (the next row is newer)
160
- trimmed_entries = entries[-self.max_entries :]
161
-
162
- with open(self.filename, "w", encoding="utf-8") as f:
163
- for entry in trimmed_entries:
164
- f.writelines(entry)
165
-
166
-
167
- class CLI:
168
- CONFIG_PATH = Path("~/.config/yaicli/config.ini").expanduser()
169
-
170
- def __init__(self, verbose: bool = False) -> None:
171
- self.verbose = verbose
172
- self.console = Console()
173
- self.bindings = KeyBindings()
174
- # Disable nonatty warning
175
- _origin_stderr = None
176
- if not sys.stdin.isatty():
177
- _origin_stderr = sys.stderr
178
- sys.stderr = open(devnull, "w")
179
- self.session = PromptSession(key_bindings=self.bindings)
180
- # Restore stderr
181
- if _origin_stderr:
182
- sys.stderr.close()
183
- sys.stderr = _origin_stderr
184
- self.config = {}
185
- self.history: list[dict[str, str]] = []
186
- self.max_history_length = 25
187
- self.current_mode = TEMP_MODE
188
-
189
- def prepare_chat_loop(self) -> None:
190
- """Setup key bindings and history for chat mode"""
191
- self._setup_key_bindings()
192
- # Initialize history
193
- Path("~/.yaicli_history").expanduser().touch(exist_ok=True)
194
- self.session = PromptSession(
195
- key_bindings=self.bindings,
196
- # completer=WordCompleter(["/clear", "/exit", "/his"]),
197
- complete_while_typing=True,
198
- history=LimitedFileHistory(
199
- Path("~/.yaicli_history").expanduser(), max_entries=int(self.config["MAX_HISTORY"])
200
- ),
201
- auto_suggest=AutoSuggestFromHistory() if self.config["AUTO_SUGGEST"] else None,
202
- enable_history_search=True,
203
- )
204
-
205
- def _setup_key_bindings(self) -> None:
206
- """Setup keyboard shortcuts"""
207
-
208
- @self.bindings.add(Keys.ControlI) # Bind TAB to switch modes
209
- def _(event: KeyPressEvent) -> None:
210
- self.current_mode = EXEC_MODE if self.current_mode == CHAT_MODE else CHAT_MODE
211
-
212
- def load_config(self) -> dict[str, str]:
213
- """Load LLM API configuration with priority:
214
- 1. Environment variables (highest priority)
215
- 2. Configuration file
216
- 3. Default values (lowest priority)
217
-
218
- Returns:
219
- dict: merged configuration
220
- """
221
- boolean_keys = ["STREAM", "AUTO_SUGGEST"]
222
- # Start with default configuration (lowest priority)
223
- merged_config: Dict[str, Any] = {k: v["value"] for k, v in DEFAULT_CONFIG_MAP.items()}
224
-
225
- # Create default config file if it doesn't exist
226
- if not self.CONFIG_PATH.exists():
227
- self.console.print("[bold yellow]Creating default configuration file.[/bold yellow]")
228
- self.CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
229
- with open(self.CONFIG_PATH, "w") as f:
230
- f.write(DEFAULT_CONFIG_INI)
231
- else:
232
- # Load from configuration file (middle priority)
233
- config_parser = CasePreservingConfigParser()
234
- config_parser.read(self.CONFIG_PATH)
235
- if "core" in config_parser:
236
- # Update with non-empty values from config file
237
- merged_config.update({k: v for k, v in config_parser["core"].items() if v.strip()})
238
-
239
- # Override with environment variables (highest priority)
240
- for key, config in DEFAULT_CONFIG_MAP.items():
241
- env_value = getenv(config["env_key"])
242
- if env_value is not None:
243
- merged_config[key] = env_value
244
- # Convert boolean values
245
- if key in boolean_keys:
246
- merged_config[key] = str(merged_config[key]).lower() == "true"
247
-
248
- self.config = merged_config
249
- return merged_config
250
-
251
- def detect_os(self) -> str:
252
- """Detect operating system + version"""
253
- if self.config.get("OS_NAME") != "auto":
254
- return self.config["OS_NAME"]
255
- current_platform = platform.system()
256
- if current_platform == "Linux":
257
- return "Linux/" + distro_name(pretty=True)
258
- if current_platform == "Windows":
259
- return "Windows " + platform.release()
260
- if current_platform == "Darwin":
261
- return "Darwin/MacOS " + platform.mac_ver()[0]
262
- return current_platform
263
-
264
- def detect_shell(self) -> str:
265
- """Detect shell name"""
266
- if self.config["SHELL_NAME"] != "auto":
267
- return self.config["SHELL_NAME"]
268
-
269
- current_platform = platform.system()
270
- if current_platform in ("Windows", "nt"):
271
- is_powershell = len(getenv("PSModulePath", "").split(pathsep)) >= 3
272
- return "powershell.exe" if is_powershell else "cmd.exe"
273
- return basename(getenv("SHELL", None) or "/bin/sh")
274
-
275
- def _filter_command(self, command: str) -> Optional[str]:
276
- """Filter out unwanted characters from command
277
-
278
- The LLM may return commands in markdown format with code blocks.
279
- This method removes markdown formatting from the command.
280
- It handles various formats including:
281
- - Commands surrounded by ``` (plain code blocks)
282
- - Commands with language specifiers like ```bash, ```zsh, etc.
283
- - Commands with specific examples like ```ls -al```
284
-
285
- example:
286
- ```bash\nls -la\n``` ==> ls -al
287
- ```zsh\nls -la\n``` ==> ls -al
288
- ```ls -la``` ==> ls -la
289
- ls -la ==> ls -la
290
- ```\ncd /tmp\nls -la\n``` ==> cd /tmp\nls -la
291
- ```bash\ncd /tmp\nls -la\n``` ==> cd /tmp\nls -la
292
- ```plaintext\nls -la\n``` ==> ls -la
293
- """
294
- if not command or not command.strip():
295
- return ""
296
-
297
- # Handle commands that are already without code blocks
298
- if "```" not in command:
299
- return command.strip()
300
-
301
- # Handle code blocks with or without language specifiers
302
- lines = command.strip().split("\n")
303
-
304
- # Check if it's a single-line code block like ```ls -al```
305
- if len(lines) == 1 and lines[0].startswith("```") and lines[0].endswith("```"):
306
- return lines[0][3:-3].strip()
307
-
308
- # Handle multi-line code blocks
309
- if lines[0].startswith("```"):
310
- # Remove the opening ``` line (with or without language specifier)
311
- content_lines = lines[1:]
312
-
313
- # If the last line is a closing ```, remove it
314
- if content_lines and content_lines[-1].strip() == "```":
315
- content_lines = content_lines[:-1]
316
-
317
- # Join the remaining lines and strip any extra whitespace
318
- return "\n".join(line.strip() for line in content_lines if line.strip())
319
-
320
- def _get_number_with_type(self, key, _type: type, default=None):
321
- """Get number with type from config"""
322
- try:
323
- return _type(self.config.get(key, default))
324
- except ValueError:
325
- raise ValueError(f"[red]{key} should be {_type} type.[/red]")
326
-
327
- def post(self, message: list[dict[str, str]]) -> httpx.Response:
328
- """Post message to LLM API and return response"""
329
- url = self.config.get("BASE_URL", "").rstrip("/") + "/" + self.config.get("COMPLETION_PATH", "").lstrip("/")
330
- body = {
331
- "messages": message,
332
- "model": self.config.get("MODEL", "gpt-4o"),
333
- "stream": self.config["STREAM"],
334
- "temperature": self._get_number_with_type(key="TEMPERATURE", _type=float, default="0.7"),
335
- "top_p": self._get_number_with_type(key="TOP_P", _type=float, default="1.0"),
336
- "max_tokens": self._get_number_with_type(key="MAX_TOKENS", _type=int, default="1024"),
337
- }
338
- with httpx.Client(timeout=120.0) as client:
339
- response = client.post(
340
- url,
341
- json=body,
342
- headers={"Authorization": f"Bearer {self.config.get('API_KEY', '')}"},
343
- )
344
- try:
345
- response.raise_for_status()
346
- except httpx.HTTPStatusError as e:
347
- self.console.print(f"[red]Error calling API: {e}[/red]")
348
- if self.verbose:
349
- self.console.print(f"Reason: {e}\nResponse: {response.text}")
350
- raise e
351
- return response
352
-
353
- def get_reasoning_content(self, delta: dict) -> Optional[str]:
354
- # reasoning: openrouter
355
- # reasoning_content: infi-ai/deepseek
356
- for k in ("reasoning_content", "reasoning"):
357
- if k in delta:
358
- return delta[k]
359
- return None
360
-
361
- def _parse_stream_line(self, line: Union[bytes, str]) -> Optional[dict]:
362
- """Parse a single line from the stream response"""
363
- if not line:
364
- return None
365
-
366
- line = str(line)
367
- if not line.startswith("data: "):
368
- return None
369
-
370
- line = line[6:]
371
- if line == "[DONE]":
372
- return None
373
-
374
- try:
375
- json_data = json.loads(line)
376
- if not json_data.get("choices"):
377
- return None
378
-
379
- return json_data
380
- except json.JSONDecodeError:
381
- self.console.print("[red]Error decoding response JSON[/red]")
382
- if self.verbose:
383
- self.console.print(f"[red]Error JSON data: {line}[/red]")
384
- return None
385
-
386
- def _process_reasoning_content(self, reason: str, full_completion: str, in_reasoning: bool) -> tuple[str, bool]:
387
- """Process reasoning content in the response"""
388
- if not in_reasoning:
389
- in_reasoning = True
390
- full_completion = "> Reasoning:\n> "
391
- full_completion += reason.replace("\n", "\n> ")
392
- return full_completion, in_reasoning
393
-
394
- def _process_regular_content(self, content: str, full_completion: str, in_reasoning: bool) -> tuple[str, bool]:
395
- """Process regular content in the response"""
396
- if in_reasoning:
397
- in_reasoning = False
398
- full_completion += "\n\n"
399
- full_completion += content
400
- return full_completion, in_reasoning
401
-
402
- def _print_stream(self, response: httpx.Response) -> str:
403
- """Print response from LLM in streaming mode"""
404
- self.console.print("Assistant:", style="bold green")
405
- full_content = ""
406
- in_reasoning = False
407
- cursor_chars = ["_", " "]
408
- cursor_index = 0
409
-
410
- with Live(console=self.console) as live:
411
- for line in response.iter_lines():
412
- json_data = self._parse_stream_line(line)
413
- if not json_data:
414
- continue
415
-
416
- delta = json_data["choices"][0]["delta"]
417
- reason = self.get_reasoning_content(delta)
418
-
419
- if reason is not None:
420
- full_content, in_reasoning = self._process_reasoning_content(reason, full_content, in_reasoning)
421
- else:
422
- full_content, in_reasoning = self._process_regular_content(
423
- delta.get("content", "") or "", full_content, in_reasoning
424
- )
425
-
426
- cursor = cursor_chars[cursor_index]
427
- live.update(
428
- Markdown(markup=full_content + cursor, code_theme=self.config["CODE_THEME"]),
429
- refresh=True,
430
- )
431
- cursor_index = (cursor_index + 1) % 2
432
- time.sleep(0.005) # Slow down the printing speed, avoiding screen flickering
433
- live.update(Markdown(markup=full_content, code_theme=self.config["CODE_THEME"]), refresh=True)
434
- return full_content
435
-
436
- def _print_normal(self, response: httpx.Response) -> str:
437
- """Print response from LLM in non-streaming mode"""
438
- self.console.print("Assistant:", style="bold green")
439
- full_content = jmespath.search(self.config.get("ANSWER_PATH", "choices[0].message.content"), response.json())
440
- self.console.print(Markdown(full_content + "\n", code_theme=self.config["CODE_THEME"]))
441
- return full_content
442
-
443
- def get_prompt_tokens(self) -> list[tuple[str, str]]:
444
- """Return prompt tokens for current mode"""
445
- qmark = "💬" if self.current_mode == CHAT_MODE else "🚀" if self.current_mode == EXEC_MODE else ""
446
- return [("class:qmark", qmark), ("class:question", " {} ".format(">"))]
447
-
448
- def _check_history_len(self) -> None:
449
- """Check history length and remove oldest messages if necessary"""
450
- if len(self.history) > self.max_history_length:
451
- self.history = self.history[-self.max_history_length :]
452
-
453
- def _handle_special_commands(self, user_input: str) -> Optional[bool]:
454
- """Handle special command return: True-continue loop, False-exit loop, None-non-special command"""
455
- if user_input.lower() == CMD_EXIT:
456
- return False
457
- if user_input.lower() == CMD_CLEAR and self.current_mode == CHAT_MODE:
458
- self.history.clear()
459
- self.console.print("Chat history cleared\n", style="bold yellow")
460
- return True
461
- if user_input.lower() == CMD_HISTORY:
462
- self.console.print(self.history)
463
- return True
464
- return None
465
-
466
- def _confirm_and_execute(self, content: str) -> None:
467
- """Review, edit and execute the command"""
468
- cmd = self._filter_command(content)
469
- if not cmd:
470
- self.console.print("No command generated", style="bold red")
471
- return
472
- self.console.print(Panel(cmd, title="Command", title_align="left", border_style="bold magenta", expand=False))
473
- _input = Prompt.ask(
474
- r"Execute command? \[e]dit, \[y]es, \[n]o",
475
- choices=["y", "n", "e"],
476
- default="n",
477
- case_sensitive=False,
478
- show_choices=False,
479
- )
480
- if _input == "y": # execute cmd
481
- self.console.print("Output:", style="bold green")
482
- subprocess.call(cmd, shell=True)
483
- elif _input == "e": # edit cmd
484
- cmd = prompt("Edit command, press enter to execute:\n", default=cmd)
485
- self.console.print("Output:", style="bold green")
486
- subprocess.call(cmd, shell=True)
487
-
488
- def _build_messages(self, user_input: str) -> list[dict[str, str]]:
489
- return [
490
- {"role": "system", "content": self.get_system_prompt()},
491
- *self.history,
492
- {"role": "user", "content": user_input},
493
- ]
494
-
495
- def _handle_llm_response(self, response: httpx.Response, user_input: str) -> str:
496
- """Print LLM response and update history"""
497
- content = self._print_stream(response) if self.config["STREAM"] else self._print_normal(response)
498
- self.history.extend([{"role": "user", "content": user_input}, {"role": "assistant", "content": content}])
499
- self._check_history_len()
500
- return content
501
-
502
- def _process_user_input(self, user_input: str) -> bool:
503
- """Process user input and generate response"""
504
- try:
505
- response = self.post(self._build_messages(user_input))
506
- content = self._handle_llm_response(response, user_input)
507
- if self.current_mode == EXEC_MODE:
508
- self._confirm_and_execute(content)
509
- return True
510
- except Exception as e:
511
- self.console.print(f"Error: {e}", style="red")
512
- return False
513
-
514
- def get_system_prompt(self) -> str:
515
- """Return system prompt for current mode"""
516
- prompt = SHELL_PROMPT if self.current_mode == EXEC_MODE else DEFAULT_PROMPT
517
- return prompt.format(_os=self.detect_os(), _shell=self.detect_shell())
518
-
519
- def _run_repl(self) -> None:
520
- """Run REPL loop, handling user input and generating responses, saving history, and executing commands"""
521
- self.prepare_chat_loop()
522
- self.console.print("""
523
- ██ ██ █████ ██ ██████ ██ ██
524
- ██ ██ ██ ██ ██ ██ ██ ██
525
- ████ ███████ ██ ██ ██ ██
526
- ██ ██ ██ ██ ██ ██ ██
527
- ██ ██ ██ ██ ██████ ███████ ██
528
- """)
529
- self.console.print("↑/↓: navigate in history")
530
- self.console.print("Press TAB to change in chat and exec mode", style="bold")
531
- self.console.print("Type /clear to clear chat history", style="bold")
532
- self.console.print("Type /his to see chat history", style="bold")
533
- self.console.print("Press Ctrl+C or type /exit to exit\n", style="bold")
534
-
535
- while True:
536
- self.console.print(Markdown("---"))
537
- user_input = self.session.prompt(self.get_prompt_tokens).strip()
538
- if not user_input:
539
- continue
540
-
541
- # Handle exit commands
542
- if user_input.lower() == CMD_EXIT:
543
- break
544
-
545
- # Handle clear command
546
- if user_input.lower() == CMD_CLEAR and self.current_mode == CHAT_MODE:
547
- self.history = []
548
- self.console.print("[bold yellow]Chat history cleared[/bold yellow]\n")
549
- continue
550
- elif user_input.lower() == CMD_HISTORY:
551
- self.console.print(self.history)
552
- continue
553
- if not self._process_user_input(user_input):
554
- continue
555
-
556
- self.console.print("[bold green]Exiting...[/bold green]")
557
-
558
- def _run_once(self, prompt: str, shell: bool = False) -> None:
559
- """Run once with given prompt"""
560
-
561
- try:
562
- response = self.post(self._build_messages(prompt))
563
- content = self._handle_llm_response(response, prompt)
564
- if shell:
565
- self._confirm_and_execute(content)
566
- except Exception as e:
567
- self.console.print(f"[red]Error: {e}[/red]")
568
-
569
- def run(self, chat: bool, shell: bool, prompt: str) -> None:
570
- """Run the CLI"""
571
- self.load_config()
572
- if self.verbose:
573
- self.console.print(f"CODE_THEME: {self.config['CODE_THEME']}")
574
- self.console.print(f"ANSWER_PATH: {self.config['ANSWER_PATH']}")
575
- self.console.print(f"COMPLETION_PATH: {self.config['COMPLETION_PATH']}")
576
- self.console.print(f"BASE_URL: {self.config['BASE_URL']}")
577
- self.console.print(f"MODEL: {self.config['MODEL']}")
578
- self.console.print(f"SHELL_NAME: {self.config['SHELL_NAME']}")
579
- self.console.print(f"OS_NAME: {self.config['OS_NAME']}")
580
- self.console.print(f"STREAM: {self.config['STREAM']}")
581
- self.console.print(f"TEMPERATURE: {self.config['TEMPERATURE']}")
582
- self.console.print(f"TOP_P: {self.config['TOP_P']}")
583
- self.console.print(f"MAX_TOKENS: {self.config['MAX_TOKENS']}")
584
- if not self.config.get("API_KEY"):
585
- self.console.print(
586
- "[yellow]API key not set. Please set in ~/.config/yaicli/config.ini or AI_API_KEY env[/]"
587
- )
588
- raise typer.Exit(code=1)
589
- if chat:
590
- self.current_mode = CHAT_MODE
591
- self._run_repl()
592
- else:
593
- self.current_mode = EXEC_MODE if shell else TEMP_MODE
594
- self._run_once(prompt, shell)
595
-
596
-
597
- @app.command()
598
- def main(
599
- ctx: typer.Context,
600
- prompt: Annotated[Optional[str], typer.Argument(show_default=False, help="The prompt send to the LLM")] = None,
601
- chat: Annotated[
602
- bool, typer.Option("--chat", "-c", help="Start in chat mode", rich_help_panel="Run Options")
603
- ] = False,
604
- shell: Annotated[
605
- bool,
606
- typer.Option(
607
- "--shell",
608
- "-s",
609
- help="Generate and execute shell command",
610
- rich_help_panel="Run Options",
611
- ),
612
- ] = False,
613
- verbose: Annotated[
614
- bool,
615
- typer.Option("--verbose", "-V", help="Show verbose information", rich_help_panel="Run Options"),
616
- ] = False,
617
- template: Annotated[bool, typer.Option("--template", help="Show the config template.")] = False,
618
- ):
619
- """yaicli - Your AI interface in cli."""
620
- # Check for stdin input (from pipe or redirect)
621
- if not sys.stdin.isatty():
622
- stdin_content = sys.stdin.read()
623
- prompt = f"{stdin_content}\n\n{prompt}"
624
-
625
- if prompt == "":
626
- typer.echo("Empty prompt, ignored")
627
- return
628
- if template:
629
- typer.echo(DEFAULT_CONFIG_INI)
630
- return
631
- if not prompt and not chat:
632
- typer.echo(ctx.get_help())
633
- return
634
-
635
- cli = CLI(verbose=verbose)
636
- cli.run(chat=chat, shell=shell, prompt=prompt or "")
637
-
638
-
639
- if __name__ == "__main__":
640
- app()