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.
yaicli/const.py CHANGED
@@ -1,10 +1,12 @@
1
1
  from enum import StrEnum
2
2
  from pathlib import Path
3
3
  from tempfile import gettempdir
4
- from typing import Any
4
+ from typing import Any, Literal, Optional
5
5
 
6
6
  from rich.console import JustifyMethod
7
7
 
8
+ BOOL_STR = Literal["true", "false", "yes", "no", "y", "n", "1", "0", "on", "off"]
9
+
8
10
 
9
11
  class JustifyEnum(StrEnum):
10
12
  DEFAULT = "default"
@@ -22,36 +24,41 @@ CMD_SAVE_CHAT = "/save"
22
24
  CMD_LOAD_CHAT = "/load"
23
25
  CMD_LIST_CHATS = "/list"
24
26
  CMD_DELETE_CHAT = "/del"
27
+ CMD_HELP = ("/help", "?")
25
28
 
26
29
  EXEC_MODE = "exec"
27
30
  CHAT_MODE = "chat"
28
31
  TEMP_MODE = "temp"
29
32
  CODE_MODE = "code"
30
33
 
34
+ HISTORY_FILE = Path("~/.yaicli_history").expanduser()
31
35
  CONFIG_PATH = Path("~/.config/yaicli/config.ini").expanduser()
32
36
  ROLES_DIR = CONFIG_PATH.parent / "roles"
37
+ FUNCTIONS_DIR = CONFIG_PATH.parent / "functions"
33
38
 
34
39
  # Default configuration values
35
40
  DEFAULT_CODE_THEME = "monokai"
36
- DEFAULT_COMPLETION_PATH = "chat/completions"
37
- DEFAULT_ANSWER_PATH = "choices[0].message.content"
38
41
  DEFAULT_PROVIDER = "openai"
39
42
  DEFAULT_BASE_URL = "https://api.openai.com/v1"
40
43
  DEFAULT_MODEL = "gpt-4o"
41
44
  DEFAULT_SHELL_NAME = "auto"
42
45
  DEFAULT_OS_NAME = "auto"
43
- DEFAULT_STREAM = "true"
44
- DEFAULT_TEMPERATURE: float = 0.7
46
+ DEFAULT_STREAM: BOOL_STR = "true"
47
+ DEFAULT_TEMPERATURE: float = 0.5
45
48
  DEFAULT_TOP_P: float = 1.0
46
49
  DEFAULT_MAX_TOKENS: int = 1024
47
50
  DEFAULT_MAX_HISTORY: int = 500
48
- DEFAULT_AUTO_SUGGEST = "true"
49
- DEFAULT_SHOW_REASONING = "true"
51
+ DEFAULT_AUTO_SUGGEST: BOOL_STR = "true"
52
+ DEFAULT_SHOW_REASONING: BOOL_STR = "true"
50
53
  DEFAULT_TIMEOUT: int = 60
51
54
  DEFAULT_INTERACTIVE_ROUND: int = 25
52
- DEFAULT_CHAT_HISTORY_DIR = Path(gettempdir()) / "yaicli/chats"
55
+ DEFAULT_CHAT_HISTORY_DIR: Path = Path(gettempdir()) / "yaicli/chats"
53
56
  DEFAULT_MAX_SAVED_CHATS = 20
54
57
  DEFAULT_JUSTIFY: JustifyMethod = "default"
58
+ DEFAULT_ROLE_MODIFY_WARNING: BOOL_STR = "true"
59
+ DEFAULT_ENABLE_FUNCTIONS: BOOL_STR = "true"
60
+ DEFAULT_SHOW_FUNCTION_OUTPUT: BOOL_STR = "true"
61
+ DEFAULT_REASONING_EFFORT: Optional[Literal["low", "high", "medium"]] = None
55
62
 
56
63
 
57
64
  class EventTypeEnum(StrEnum):
@@ -62,14 +69,18 @@ class EventTypeEnum(StrEnum):
62
69
  REASONING_END = "reasoning_end"
63
70
  CONTENT = "content"
64
71
  FINISH = "finish"
72
+ TOOL_CALL_START = "tool_call_start"
73
+ TOOL_CALL_DELTA = "tool_call_delta"
74
+ TOOL_CALL_END = "tool_call_end"
75
+ TOOL_RESULT = "tool_result"
76
+ TOOL_CALLS_FINISH = "tool_calls_finish"
65
77
 
66
78
 
67
- SHELL_PROMPT = """Your are a Shell Command Generator named YAICLI.
68
- Generate a command EXCLUSIVELY for {_os} OS with {_shell} shell.
69
- If details are missing, offer the most logical solution.
70
- Ensure the output is a valid shell command.
71
- Combine multiple steps with `&&` when possible.
72
- Supply plain text only, avoiding Markdown formatting."""
79
+ SHELL_PROMPT = """You are YAICLI, a shell command generator.
80
+ The context conversation may contain other types of messages,
81
+ but you should only respond with a single valid {_shell} shell command for {_os}.
82
+ Do not include any explanations, comments, or formatting — only the command as plain text, avoiding Markdown formatting.
83
+ """
73
84
 
74
85
  DEFAULT_PROMPT = """
75
86
  You are YAICLI, a system management and programing assistant,
@@ -113,15 +124,13 @@ DEFAULT_CONFIG_MAP = {
113
124
  # System detection hints
114
125
  "SHELL_NAME": {"value": DEFAULT_SHELL_NAME, "env_key": "YAI_SHELL_NAME", "type": str},
115
126
  "OS_NAME": {"value": DEFAULT_OS_NAME, "env_key": "YAI_OS_NAME", "type": str},
116
- # API paths (usually no need to change for OpenAI compatible APIs)
117
- "COMPLETION_PATH": {"value": DEFAULT_COMPLETION_PATH, "env_key": "YAI_COMPLETION_PATH", "type": str},
118
- "ANSWER_PATH": {"value": DEFAULT_ANSWER_PATH, "env_key": "YAI_ANSWER_PATH", "type": str},
119
127
  # API call parameters
120
128
  "STREAM": {"value": DEFAULT_STREAM, "env_key": "YAI_STREAM", "type": bool},
121
129
  "TEMPERATURE": {"value": DEFAULT_TEMPERATURE, "env_key": "YAI_TEMPERATURE", "type": float},
122
130
  "TOP_P": {"value": DEFAULT_TOP_P, "env_key": "YAI_TOP_P", "type": float},
123
131
  "MAX_TOKENS": {"value": DEFAULT_MAX_TOKENS, "env_key": "YAI_MAX_TOKENS", "type": int},
124
132
  "TIMEOUT": {"value": DEFAULT_TIMEOUT, "env_key": "YAI_TIMEOUT", "type": int},
133
+ "REASONING_EFFORT": {"value": DEFAULT_REASONING_EFFORT, "env_key": "YAI_REASONING_EFFORT", "type": str},
125
134
  "INTERACTIVE_ROUND": {
126
135
  "value": DEFAULT_INTERACTIVE_ROUND,
127
136
  "env_key": "YAI_INTERACTIVE_ROUND",
@@ -136,6 +145,15 @@ DEFAULT_CONFIG_MAP = {
136
145
  # Chat history settings
137
146
  "CHAT_HISTORY_DIR": {"value": DEFAULT_CHAT_HISTORY_DIR, "env_key": "YAI_CHAT_HISTORY_DIR", "type": str},
138
147
  "MAX_SAVED_CHATS": {"value": DEFAULT_MAX_SAVED_CHATS, "env_key": "YAI_MAX_SAVED_CHATS", "type": int},
148
+ # Role settings
149
+ "ROLE_MODIFY_WARNING": {"value": DEFAULT_ROLE_MODIFY_WARNING, "env_key": "YAI_ROLE_MODIFY_WARNING", "type": bool},
150
+ # Function settings
151
+ "ENABLE_FUNCTIONS": {"value": DEFAULT_ENABLE_FUNCTIONS, "env_key": "YAI_ENABLE_FUNCTIONS", "type": bool},
152
+ "SHOW_FUNCTION_OUTPUT": {
153
+ "value": DEFAULT_SHOW_FUNCTION_OUTPUT,
154
+ "env_key": "YAI_SHOW_FUNCTION_OUTPUT",
155
+ "type": bool,
156
+ },
139
157
  }
140
158
 
141
159
  DEFAULT_CONFIG_INI = f"""[core]
@@ -148,10 +166,6 @@ MODEL={DEFAULT_CONFIG_MAP["MODEL"]["value"]}
148
166
  SHELL_NAME={DEFAULT_CONFIG_MAP["SHELL_NAME"]["value"]}
149
167
  OS_NAME={DEFAULT_CONFIG_MAP["OS_NAME"]["value"]}
150
168
 
151
- # API paths (usually no need to change for OpenAI compatible APIs)
152
- COMPLETION_PATH={DEFAULT_CONFIG_MAP["COMPLETION_PATH"]["value"]}
153
- ANSWER_PATH={DEFAULT_CONFIG_MAP["ANSWER_PATH"]["value"]}
154
-
155
169
  # true: streaming response, false: non-streaming
156
170
  STREAM={DEFAULT_CONFIG_MAP["STREAM"]["value"]}
157
171
 
@@ -160,6 +174,7 @@ TEMPERATURE={DEFAULT_CONFIG_MAP["TEMPERATURE"]["value"]}
160
174
  TOP_P={DEFAULT_CONFIG_MAP["TOP_P"]["value"]}
161
175
  MAX_TOKENS={DEFAULT_CONFIG_MAP["MAX_TOKENS"]["value"]}
162
176
  TIMEOUT={DEFAULT_CONFIG_MAP["TIMEOUT"]["value"]}
177
+ REASONING_EFFORT=
163
178
 
164
179
  # Interactive mode parameters
165
180
  INTERACTIVE_ROUND={DEFAULT_CONFIG_MAP["INTERACTIVE_ROUND"]["value"]}
@@ -177,4 +192,14 @@ JUSTIFY={DEFAULT_CONFIG_MAP["JUSTIFY"]["value"]}
177
192
  # Chat history settings
178
193
  CHAT_HISTORY_DIR={DEFAULT_CONFIG_MAP["CHAT_HISTORY_DIR"]["value"]}
179
194
  MAX_SAVED_CHATS={DEFAULT_CONFIG_MAP["MAX_SAVED_CHATS"]["value"]}
195
+
196
+ # Role settings
197
+ # Set to false to disable warnings about modified built-in roles
198
+ ROLE_MODIFY_WARNING={DEFAULT_CONFIG_MAP["ROLE_MODIFY_WARNING"]["value"]}
199
+
200
+ # Function settings
201
+ # Set to false to disable sending functions in API requests
202
+ ENABLE_FUNCTIONS={DEFAULT_CONFIG_MAP["ENABLE_FUNCTIONS"]["value"]}
203
+ # Set to false to disable showing function output in the response
204
+ SHOW_FUNCTION_OUTPUT={DEFAULT_CONFIG_MAP["SHOW_FUNCTION_OUTPUT"]["value"]}
180
205
  """
yaicli/entry.py CHANGED
@@ -3,15 +3,15 @@ from typing import Annotated, Any, Optional
3
3
 
4
4
  import typer
5
5
 
6
- from yaicli.chat_manager import FileChatManager
7
- from yaicli.cli import CLI
8
- from yaicli.config import cfg
9
- from yaicli.const import DEFAULT_CONFIG_INI, DefaultRoleNames, JustifyEnum
10
- from yaicli.roles import RoleManager
6
+ from .chat import FileChatManager
7
+ from .config import cfg
8
+ from .const import DEFAULT_CONFIG_INI, DefaultRoleNames, JustifyEnum
9
+ from .functions import install_functions, print_functions
10
+ from .role import RoleManager
11
11
 
12
12
  app = typer.Typer(
13
13
  name="yaicli",
14
- help="YAICLI - Yet Another AI CLI Interface.",
14
+ help="YAICLI - Your AI assistant in the command line.",
15
15
  context_settings={"help_option_names": ["-h", "--help"]},
16
16
  pretty_exceptions_enable=False, # Let the CLI handle errors gracefully
17
17
  rich_markup_mode="rich", # Render rich text in help messages
@@ -73,6 +73,14 @@ def main(
73
73
  min=1,
74
74
  callback=override_config,
75
75
  ),
76
+ stream: bool = typer.Option( # noqa: F841
77
+ cfg["STREAM"],
78
+ "--stream/--no-stream",
79
+ help=f"Specify whether to stream the response. [dim](default: {'stream' if cfg['STREAM'] else 'no-stream'})[/dim]",
80
+ rich_help_panel="LLM Options",
81
+ show_default=False,
82
+ callback=override_config,
83
+ ),
76
84
  # ------------------- Role Options -------------------
77
85
  role: str = typer.Option(
78
86
  DefaultRoleNames.DEFAULT,
@@ -118,7 +126,7 @@ def main(
118
126
  help="Start in interactive chat mode.",
119
127
  rich_help_panel="Chat Options",
120
128
  ),
121
- # ------------------- Shell Options -------------------
129
+ # # ------------------- Shell Options -------------------
122
130
  shell: bool = typer.Option(
123
131
  False,
124
132
  "--shell",
@@ -126,7 +134,7 @@ def main(
126
134
  help="Generate and optionally execute a shell command (non-interactive).",
127
135
  rich_help_panel="Shell Options",
128
136
  ),
129
- # ------------------- Code Options -------------------
137
+ # # ------------------- Code Options -------------------
130
138
  code: bool = typer.Option(
131
139
  False,
132
140
  "--code",
@@ -157,7 +165,8 @@ def main(
157
165
  ),
158
166
  show_reasoning: bool = typer.Option( # noqa: F841
159
167
  cfg["SHOW_REASONING"],
160
- help=f"Show reasoning content from the LLM. [dim](default: {cfg['SHOW_REASONING']})[/dim]",
168
+ "--show-reasoning/--hide-reasoning",
169
+ help=f"Show reasoning content from the LLM. [dim](default: {'show' if cfg['SHOW_REASONING'] else 'hide'})[/dim]",
161
170
  rich_help_panel="Other Options",
162
171
  show_default=False,
163
172
  callback=override_config,
@@ -170,6 +179,37 @@ def main(
170
179
  rich_help_panel="Other Options",
171
180
  callback=override_config,
172
181
  ),
182
+ # ------------------- Function Options -------------------
183
+ install_functions: bool = typer.Option( # noqa: F841
184
+ False,
185
+ "--install-functions",
186
+ help="Install default functions.",
187
+ rich_help_panel="Function Options",
188
+ callback=install_functions,
189
+ ),
190
+ list_functions: bool = typer.Option( # noqa: F841
191
+ False,
192
+ "--list-functions",
193
+ help="List all available functions.",
194
+ rich_help_panel="Function Options",
195
+ callback=print_functions,
196
+ ),
197
+ enable_functions: bool = typer.Option( # noqa: F841
198
+ cfg["ENABLE_FUNCTIONS"],
199
+ "--enable-functions/--disable-functions",
200
+ help=f"Enable/disable function calling in API requests [dim](default: {'enabled' if cfg['ENABLE_FUNCTIONS'] else 'disabled'})[/dim]",
201
+ rich_help_panel="Function Options",
202
+ show_default=False,
203
+ callback=override_config,
204
+ ),
205
+ show_function_output: bool = typer.Option( # noqa: F841
206
+ cfg["SHOW_FUNCTION_OUTPUT"],
207
+ "--show-function-output/--hide-function-output",
208
+ help=f"Show the output of functions [dim](default: {'show' if cfg['SHOW_FUNCTION_OUTPUT'] else 'hide'})[/dim]",
209
+ rich_help_panel="Function Options",
210
+ show_default=False,
211
+ callback=override_config,
212
+ ),
173
213
  ):
174
214
  """YAICLI: Your AI assistant in the command line.
175
215
 
@@ -179,7 +219,7 @@ def main(
179
219
  print(DEFAULT_CONFIG_INI)
180
220
  raise typer.Exit()
181
221
 
182
- # Combine prompt argument with stdin content if available
222
+ # # Combine prompt argument with stdin content if available
183
223
  final_prompt = prompt
184
224
  if not sys.stdin.isatty():
185
225
  stdin_content = sys.stdin.read().strip()
@@ -194,40 +234,29 @@ def main(
194
234
  if chat:
195
235
  print("Warning: --chat is ignored when stdin was redirected.")
196
236
  chat = False
237
+ if not any([final_prompt, chat]):
238
+ print(ctx.get_help())
239
+ return
197
240
 
198
- # Basic validation for conflicting options or missing prompt
199
- if not any([final_prompt, chat, list_chats, list_roles, create_role]):
200
- # If no prompt, not starting chat, and not listing chats or roles, show help
201
- typer.echo(ctx.get_help())
202
- raise typer.Exit()
203
-
204
- # Use build-in role for --shell or --code mode
241
+ # # Use build-in role for --shell or --code mode
205
242
  if role and role != DefaultRoleNames.DEFAULT and (shell or code):
206
243
  print("Warning: --role is ignored when --shell or --code is used.")
207
244
  role = DefaultRoleNames.DEFAULT
208
245
 
209
- if code:
210
- role = DefaultRoleNames.CODER
211
-
212
- try:
213
- # Instantiate the main CLI class with the specified role
214
- cli_instance = CLI(verbose=verbose, role=role)
215
-
216
- # Run the appropriate mode
217
- cli_instance.run(
218
- chat=chat,
219
- shell=shell,
220
- input=final_prompt,
221
- role=role,
222
- )
223
- except Exception as e:
224
- # Catch potential errors during CLI initialization or run
225
- print(f"An error occurred: {e}")
226
- if verbose:
227
- import traceback
228
-
229
- traceback.print_exc()
230
- raise typer.Exit(code=1)
246
+ from yaicli.cli import CLI
247
+
248
+ role = CLI.evaluate_role_name(code, shell, role)
249
+
250
+ # Instantiate the main CLI class with the specified role
251
+ cli = CLI(verbose=verbose, role=role)
252
+
253
+ # Run the appropriate mode
254
+ cli.run(
255
+ chat=chat,
256
+ shell=shell,
257
+ code=code,
258
+ user_input=final_prompt,
259
+ )
231
260
 
232
261
 
233
262
  if __name__ == "__main__":
yaicli/exceptions.py CHANGED
@@ -1,46 +1,18 @@
1
- class YAICLIException(Exception):
2
- """Base exception for YAICLI"""
1
+ class YaicliError(Exception):
2
+ """Base exception for yaicli"""
3
3
 
4
4
  def __init__(self, message: str):
5
5
  self.message = message
6
6
  super().__init__(self.message)
7
7
 
8
8
 
9
- ###################################
10
- # Role exceptions
11
- class RoleNotFoundError(YAICLIException):
12
- """Exception raised when a role is not found"""
9
+ class ChatSaveError(YaicliError):
10
+ """Error saving chat"""
13
11
 
14
- pass
15
12
 
13
+ class ChatLoadError(YaicliError):
14
+ """Error loading chat"""
16
15
 
17
- class RoleAlreadyExistsError(YAICLIException):
18
- """Exception raised when a role already exists"""
19
16
 
20
- pass
21
-
22
-
23
- class RoleCreationError(YAICLIException):
24
- """Exception raised when a role creation fails"""
25
-
26
- pass
27
-
28
-
29
- ###################################
30
- # Chat exceptions
31
- class ChatNotFoundError(YAICLIException):
32
- """Exception raised when a chat is not found"""
33
-
34
- pass
35
-
36
-
37
- class ChatSaveError(YAICLIException):
38
- """Exception raised when a chat save fails"""
39
-
40
- pass
41
-
42
-
43
- class ChatDeleteError(YAICLIException):
44
- """Exception raised when a chat delete fails"""
45
-
46
- pass
17
+ class ChatDeleteError(YaicliError):
18
+ """Error deleting chat"""
@@ -0,0 +1,39 @@
1
+ import shutil
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from ..console import get_console
6
+ from ..const import FUNCTIONS_DIR
7
+ from ..utils import option_callback
8
+
9
+ console = get_console()
10
+
11
+
12
+ @option_callback
13
+ def install_functions(cls, _: Any) -> None:
14
+ """Install buildin functions"""
15
+ cur_dir = Path(__file__).absolute().parent
16
+ buildin_dir = cur_dir / "buildin"
17
+ buildin_funcs = [Path(path) for path in buildin_dir.glob("*.py")]
18
+ console.print("Installing buildin functions...")
19
+ if not FUNCTIONS_DIR.exists():
20
+ FUNCTIONS_DIR.mkdir(parents=True)
21
+ for file in buildin_funcs:
22
+ if (FUNCTIONS_DIR / file.name).exists():
23
+ # Skip if function already exists
24
+ console.print(f"Function {file.name} already exists, skipping.")
25
+ continue
26
+ shutil.copy(file, FUNCTIONS_DIR, follow_symlinks=True)
27
+ console.print(f"Installed {FUNCTIONS_DIR}/{file.name}")
28
+
29
+
30
+ @option_callback
31
+ def print_functions(cls, _: Any) -> None:
32
+ """List all available buildin functions"""
33
+ if not FUNCTIONS_DIR.exists():
34
+ console.print("No installed functions found.")
35
+ return
36
+ for file in FUNCTIONS_DIR.glob("*.py"):
37
+ if file.name.startswith("_"):
38
+ continue
39
+ console.print(file)
@@ -0,0 +1,47 @@
1
+ import subprocess
2
+
3
+ from instructor import OpenAISchema
4
+ from pydantic import Field
5
+
6
+
7
+ class Function(OpenAISchema):
8
+ """
9
+ Execute a shell command and return the output (result).
10
+ """
11
+
12
+ shell_command: str = Field(
13
+ ...,
14
+ json_schema_extra={
15
+ "example": "ls -la",
16
+ },
17
+ description="Shell command to execute.",
18
+ )
19
+
20
+ class Config:
21
+ title = "execute_shell_command"
22
+
23
+ @classmethod
24
+ def execute(cls, shell_command: str) -> str:
25
+ """
26
+ Execute a shell command and return the output (result).
27
+
28
+ Args:
29
+ shell_command (str): shell command to execute.
30
+
31
+ Returns:
32
+ str: exit code and output string.
33
+ """
34
+ # Optional security check
35
+ dangerous_commands = ["rm -rf", "mkfs", "dd"]
36
+ if any(cmd in shell_command for cmd in dangerous_commands):
37
+ return "Error: Dangerous command detected."
38
+
39
+ try:
40
+ process = subprocess.Popen(
41
+ shell_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
42
+ )
43
+ output, _ = process.communicate()
44
+ exit_code = process.returncode
45
+ return f"Exit code: {exit_code}, Output:\n{output}"
46
+ except Exception as e:
47
+ return f"Error: {str(e)}"