yaicli 0.2.0__py3-none-any.whl → 0.3.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/console.py ADDED
@@ -0,0 +1,66 @@
1
+ from typing import Any, Optional, Union
2
+
3
+ from rich.console import Console, JustifyMethod, OverflowMethod
4
+ from rich.style import Style
5
+
6
+ from yaicli.config import cfg
7
+ from yaicli.const import DEFAULT_JUSTIFY
8
+
9
+ _console = None
10
+
11
+
12
+ class YaiConsole(Console):
13
+ """Custom Console class that defaults to the configured justify value."""
14
+
15
+ def __init__(self, *args, **kwargs):
16
+ super().__init__(*args, **kwargs)
17
+ self._default_justify: JustifyMethod = DEFAULT_JUSTIFY
18
+ if "JUSTIFY" in cfg:
19
+ self._default_justify = cfg["JUSTIFY"]
20
+
21
+ def print(
22
+ self,
23
+ *objects: Any,
24
+ sep: str = " ",
25
+ end: str = "\n",
26
+ style: Optional[Union[str, Style]] = None,
27
+ justify: Optional[JustifyMethod] = None,
28
+ overflow: Optional[OverflowMethod] = None,
29
+ no_wrap: Optional[bool] = None,
30
+ emoji: Optional[bool] = None,
31
+ markup: Optional[bool] = None,
32
+ highlight: Optional[bool] = None,
33
+ width: Optional[int] = None,
34
+ height: Optional[int] = None,
35
+ crop: bool = True,
36
+ soft_wrap: Optional[bool] = None,
37
+ new_line_start: bool = False,
38
+ ):
39
+ """Override the print method to default to the configured justify value."""
40
+ if justify is None:
41
+ justify = self._default_justify
42
+ return super().print(
43
+ *objects,
44
+ sep=sep,
45
+ end=end,
46
+ style=style,
47
+ justify=justify,
48
+ overflow=overflow,
49
+ no_wrap=no_wrap,
50
+ emoji=emoji,
51
+ markup=markup,
52
+ highlight=highlight,
53
+ width=width,
54
+ height=height,
55
+ crop=crop,
56
+ soft_wrap=soft_wrap,
57
+ new_line_start=new_line_start,
58
+ )
59
+
60
+
61
+ def get_console() -> YaiConsole:
62
+ """Use a singleton pattern to ensure only one instance of Console is created."""
63
+ global _console
64
+ if _console is None:
65
+ _console = YaiConsole()
66
+ return _console
yaicli/const.py CHANGED
@@ -1,6 +1,18 @@
1
1
  from enum import StrEnum
2
2
  from pathlib import Path
3
3
  from tempfile import gettempdir
4
+ from typing import Any
5
+
6
+ from rich.console import JustifyMethod
7
+
8
+
9
+ class JustifyEnum(StrEnum):
10
+ DEFAULT = "default"
11
+ LEFT = "left"
12
+ CENTER = "center"
13
+ RIGHT = "right"
14
+ FULL = "full"
15
+
4
16
 
5
17
  CMD_CLEAR = "/clear"
6
18
  CMD_EXIT = "/exit"
@@ -14,8 +26,12 @@ CMD_DELETE_CHAT = "/del"
14
26
  EXEC_MODE = "exec"
15
27
  CHAT_MODE = "chat"
16
28
  TEMP_MODE = "temp"
29
+ CODE_MODE = "code"
17
30
 
18
31
  CONFIG_PATH = Path("~/.config/yaicli/config.ini").expanduser()
32
+ ROLES_DIR = CONFIG_PATH.parent / "roles"
33
+
34
+ # Default configuration values
19
35
  DEFAULT_CODE_THEME = "monokai"
20
36
  DEFAULT_COMPLETION_PATH = "chat/completions"
21
37
  DEFAULT_ANSWER_PATH = "choices[0].message.content"
@@ -30,10 +46,12 @@ DEFAULT_TOP_P: float = 1.0
30
46
  DEFAULT_MAX_TOKENS: int = 1024
31
47
  DEFAULT_MAX_HISTORY: int = 500
32
48
  DEFAULT_AUTO_SUGGEST = "true"
49
+ DEFAULT_SHOW_REASONING = "true"
33
50
  DEFAULT_TIMEOUT: int = 60
34
51
  DEFAULT_INTERACTIVE_ROUND: int = 25
35
52
  DEFAULT_CHAT_HISTORY_DIR = Path(gettempdir()) / "yaicli/chats"
36
53
  DEFAULT_MAX_SAVED_CHATS = 20
54
+ DEFAULT_JUSTIFY: JustifyMethod = "default"
37
55
 
38
56
 
39
57
  class EventTypeEnum(StrEnum):
@@ -46,20 +64,41 @@ class EventTypeEnum(StrEnum):
46
64
  FINISH = "finish"
47
65
 
48
66
 
49
- SHELL_PROMPT = """Your are a Shell Command Generator.
67
+ SHELL_PROMPT = """Your are a Shell Command Generator named YAICLI.
50
68
  Generate a command EXCLUSIVELY for {_os} OS with {_shell} shell.
51
69
  If details are missing, offer the most logical solution.
52
70
  Ensure the output is a valid shell command.
53
71
  Combine multiple steps with `&&` when possible.
54
72
  Supply plain text only, avoiding Markdown formatting."""
55
73
 
56
- DEFAULT_PROMPT = (
57
- "You are YAICLI, a system management and programing assistant, "
58
- "You are managing {_os} operating system with {_shell} shell. "
59
- "Your responses should be concise and use Markdown format (but dont't use ```markdown), "
60
- "unless the user explicitly requests more details."
74
+ DEFAULT_PROMPT = """
75
+ You are YAICLI, a system management and programing assistant,
76
+ You are managing {_os} operating system with {_shell} shell.
77
+ Your responses should be concise and use Markdown format (but dont't use ```markdown),
78
+ unless the user explicitly requests more details.
79
+ """
80
+
81
+ CODER_PROMPT = (
82
+ "You are YAICLI, a code assistant. "
83
+ "You are helping with programming tasks. "
84
+ "Your responses must ONLY contain code, with NO explanation, NO markdown formatting, and NO preamble. "
85
+ "If user does not specify the language, provide Python code. "
86
+ "Do not wrap code in markdown code blocks (```) or language indicators."
61
87
  )
62
88
 
89
+
90
+ class DefaultRoleNames(StrEnum):
91
+ SHELL = "Shell Command Generator"
92
+ DEFAULT = "DEFAULT"
93
+ CODER = "Code Assistant"
94
+
95
+
96
+ DEFAULT_ROLES: dict[str, dict[str, Any]] = {
97
+ DefaultRoleNames.SHELL: {"name": DefaultRoleNames.SHELL, "prompt": SHELL_PROMPT},
98
+ DefaultRoleNames.DEFAULT: {"name": DefaultRoleNames.DEFAULT, "prompt": DEFAULT_PROMPT},
99
+ DefaultRoleNames.CODER: {"name": DefaultRoleNames.CODER, "prompt": CODER_PROMPT},
100
+ }
101
+
63
102
  # DEFAULT_CONFIG_MAP is a dictionary of the configuration options.
64
103
  # The key is the name of the configuration option.
65
104
  # The value is a dictionary with the following keys:
@@ -92,6 +131,8 @@ DEFAULT_CONFIG_MAP = {
92
131
  "CODE_THEME": {"value": DEFAULT_CODE_THEME, "env_key": "YAI_CODE_THEME", "type": str},
93
132
  "MAX_HISTORY": {"value": DEFAULT_MAX_HISTORY, "env_key": "YAI_MAX_HISTORY", "type": int},
94
133
  "AUTO_SUGGEST": {"value": DEFAULT_AUTO_SUGGEST, "env_key": "YAI_AUTO_SUGGEST", "type": bool},
134
+ "SHOW_REASONING": {"value": DEFAULT_SHOW_REASONING, "env_key": "YAI_SHOW_REASONING", "type": bool},
135
+ "JUSTIFY": {"value": DEFAULT_JUSTIFY, "env_key": "YAI_JUSTIFY", "type": str},
95
136
  # Chat history settings
96
137
  "CHAT_HISTORY_DIR": {"value": DEFAULT_CHAT_HISTORY_DIR, "env_key": "YAI_CHAT_HISTORY_DIR", "type": str},
97
138
  "MAX_SAVED_CHATS": {"value": DEFAULT_MAX_SAVED_CHATS, "env_key": "YAI_MAX_SAVED_CHATS", "type": int},
@@ -128,6 +169,10 @@ CODE_THEME={DEFAULT_CONFIG_MAP["CODE_THEME"]["value"]}
128
169
  # Max entries kept in history file
129
170
  MAX_HISTORY={DEFAULT_CONFIG_MAP["MAX_HISTORY"]["value"]}
130
171
  AUTO_SUGGEST={DEFAULT_CONFIG_MAP["AUTO_SUGGEST"]["value"]}
172
+ # Print reasoning content or not
173
+ SHOW_REASONING={DEFAULT_CONFIG_MAP["SHOW_REASONING"]["value"]}
174
+ # Text alignment (default, left, center, right, full)
175
+ JUSTIFY={DEFAULT_CONFIG_MAP["JUSTIFY"]["value"]}
131
176
 
132
177
  # Chat history settings
133
178
  CHAT_HISTORY_DIR={DEFAULT_CONFIG_MAP["CHAT_HISTORY_DIR"]["value"]}
yaicli/entry.py CHANGED
@@ -1,64 +1,179 @@
1
1
  import sys
2
- from typing import Annotated, Optional
2
+ from typing import Annotated, Any, Optional
3
3
 
4
4
  import typer
5
5
 
6
+ from yaicli.chat_manager import FileChatManager
6
7
  from yaicli.cli import CLI
7
- from yaicli.const import DEFAULT_CONFIG_INI
8
+ from yaicli.config import cfg
9
+ from yaicli.const import DEFAULT_CONFIG_INI, DefaultRoleNames, JustifyEnum
10
+ from yaicli.roles import RoleManager
8
11
 
9
12
  app = typer.Typer(
10
13
  name="yaicli",
11
14
  help="YAICLI - Yet Another AI CLI Interface.",
12
15
  context_settings={"help_option_names": ["-h", "--help"]},
13
16
  pretty_exceptions_enable=False, # Let the CLI handle errors gracefully
14
- add_completion=False, # Disable default shell completion for now
17
+ rich_markup_mode="rich", # Render rich text in help messages
15
18
  )
16
19
 
17
20
 
21
+ def override_config(
22
+ ctx: typer.Context, # noqa: F841
23
+ param: typer.CallbackParam,
24
+ value: Any,
25
+ ):
26
+ """Override config with input value if value not equal to option default."""
27
+ if value != param.default and isinstance(param.name, str):
28
+ cfg[param.name.upper()] = value
29
+ return value
30
+
31
+
18
32
  @app.command()
19
33
  def main(
20
34
  ctx: typer.Context,
21
35
  prompt: Annotated[
22
36
  Optional[str], typer.Argument(help="The prompt to send to the LLM. Reads from stdin if available.")
23
37
  ] = None,
24
- chat: Annotated[
25
- bool, typer.Option("--chat", "-c", help="Start in interactive chat mode.", rich_help_panel="Chat Options")
26
- ] = False,
27
- shell: Annotated[
28
- bool,
29
- typer.Option(
30
- "--shell",
31
- "-s",
32
- help="Generate and optionally execute a shell command (non-interactive).",
33
- rich_help_panel="Shell Options",
34
- ),
35
- ] = False,
36
- list_chats: Annotated[
37
- bool,
38
- typer.Option(
39
- "--list-chats",
40
- "-lc",
41
- help="List saved chat sessions.",
42
- rich_help_panel="Chat Options",
43
- ),
44
- ] = False,
45
- verbose: Annotated[
46
- bool,
47
- typer.Option(
48
- "--verbose", "-V", help="Show verbose output (e.g., loaded config).", rich_help_panel="Other Options"
49
- ),
50
- ] = False,
51
- template: Annotated[
52
- bool,
53
- typer.Option(
54
- "--template", help="Show the default config file template and exit.", rich_help_panel="Other Options"
55
- ),
56
- ] = False,
57
- ): # Removed trailing comma
38
+ # ------------------- LLM Options -------------------
39
+ model: str = typer.Option( # noqa: F841
40
+ "",
41
+ "--model",
42
+ "-M",
43
+ help="Specify the model to use.",
44
+ rich_help_panel="LLM Options",
45
+ callback=override_config,
46
+ ),
47
+ temperature: float = typer.Option( # noqa: F841
48
+ cfg["TEMPERATURE"],
49
+ "--temperature",
50
+ "-T",
51
+ help="Specify the temperature to use.",
52
+ rich_help_panel="LLM Options",
53
+ min=0.0,
54
+ max=2.0,
55
+ callback=override_config,
56
+ ),
57
+ top_p: float = typer.Option( # noqa: F841
58
+ cfg["TOP_P"],
59
+ "--top-p",
60
+ "-P",
61
+ help="Specify the top-p to use.",
62
+ rich_help_panel="LLM Options",
63
+ min=0.0,
64
+ max=1.0,
65
+ callback=override_config,
66
+ ),
67
+ max_tokens: int = typer.Option( # noqa: F841
68
+ cfg["MAX_TOKENS"],
69
+ "--max-tokens",
70
+ "-M",
71
+ help="Specify the max tokens to use.",
72
+ rich_help_panel="LLM Options",
73
+ min=1,
74
+ callback=override_config,
75
+ ),
76
+ # ------------------- Role Options -------------------
77
+ role: str = typer.Option(
78
+ DefaultRoleNames.DEFAULT,
79
+ "--role",
80
+ "-r",
81
+ help="Specify the assistant role to use.",
82
+ rich_help_panel="Role Options",
83
+ callback=RoleManager.check_id_ok,
84
+ ),
85
+ create_role: str = typer.Option(
86
+ "",
87
+ "--create-role",
88
+ help="Create a new role with the specified name.",
89
+ rich_help_panel="Role Options",
90
+ callback=RoleManager.create_role_option,
91
+ ),
92
+ delete_role: str = typer.Option( # noqa: F841
93
+ "",
94
+ "--delete-role",
95
+ help="Delete a role with the specified name.",
96
+ rich_help_panel="Role Options",
97
+ callback=RoleManager.delete_role_option,
98
+ ),
99
+ list_roles: bool = typer.Option(
100
+ False,
101
+ "--list-roles",
102
+ help="List all available roles.",
103
+ rich_help_panel="Role Options",
104
+ callback=RoleManager.print_list_option,
105
+ ),
106
+ show_role: str = typer.Option( # noqa: F841
107
+ "",
108
+ "--show-role",
109
+ help="Show the role with the specified name.",
110
+ rich_help_panel="Role Options",
111
+ callback=RoleManager.show_role_option,
112
+ ),
113
+ # ------------------- Chat Options -------------------
114
+ chat: bool = typer.Option(
115
+ False,
116
+ "--chat",
117
+ "-c",
118
+ help="Start in interactive chat mode.",
119
+ rich_help_panel="Chat Options",
120
+ ),
121
+ # ------------------- Shell Options -------------------
122
+ shell: bool = typer.Option(
123
+ False,
124
+ "--shell",
125
+ "-s",
126
+ help="Generate and optionally execute a shell command (non-interactive).",
127
+ rich_help_panel="Shell Options",
128
+ ),
129
+ # ------------------- Code Options -------------------
130
+ code: bool = typer.Option(
131
+ False,
132
+ "--code",
133
+ help="Generate and optionally execute a code block (non-interactive).",
134
+ rich_help_panel="Code Options",
135
+ ),
136
+ # ------------------- Chat Options -------------------
137
+ list_chats: bool = typer.Option(
138
+ False,
139
+ "--list-chats",
140
+ help="List saved chat sessions.",
141
+ rich_help_panel="Chat Options",
142
+ callback=FileChatManager.print_list_option,
143
+ ),
144
+ # ------------------- Other Options -------------------
145
+ verbose: bool = typer.Option(
146
+ False,
147
+ "--verbose",
148
+ "-V",
149
+ help="Show verbose output (e.g., loaded config).",
150
+ rich_help_panel="Other Options",
151
+ ),
152
+ template: bool = typer.Option(
153
+ False,
154
+ "--template",
155
+ help="Show the default config file template and exit.",
156
+ rich_help_panel="Other Options",
157
+ ),
158
+ show_reasoning: bool = typer.Option( # noqa: F841
159
+ cfg["SHOW_REASONING"],
160
+ help=f"Show reasoning content from the LLM. [dim](default: {cfg['SHOW_REASONING']})[/dim]",
161
+ rich_help_panel="Other Options",
162
+ show_default=False,
163
+ callback=override_config,
164
+ ),
165
+ justify: JustifyEnum = typer.Option( # noqa: F841
166
+ cfg["JUSTIFY"],
167
+ "--justify",
168
+ "-j",
169
+ help="Specify the justify to use.",
170
+ rich_help_panel="Other Options",
171
+ callback=override_config,
172
+ ),
173
+ ):
58
174
  """YAICLI: Your AI assistant in the command line.
59
175
 
60
- Call with a PROMPT to get a direct answer, use --shell to execute as command,
61
- or use --chat for an interactive session.
176
+ Call with a PROMPT to get a direct answer, use --shell to execute as command, or use --chat for an interactive session.
62
177
  """
63
178
  if template:
64
179
  print(DEFAULT_CONFIG_INI)
@@ -75,28 +190,36 @@ def main(
75
190
  else:
76
191
  final_prompt = stdin_content
77
192
  # prompt_toolkit will raise EOFError if stdin is redirected
78
- # Set chat to False to avoid starting interactive mode
193
+ # Set chat to False to prevent starting interactive mode.
79
194
  if chat:
80
195
  print("Warning: --chat is ignored when stdin was redirected.")
81
196
  chat = False
82
197
 
83
198
  # Basic validation for conflicting options or missing prompt
84
- if not final_prompt and not chat and not list_chats:
85
- # If no prompt, not starting chat, and not listing chats, show help
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
86
201
  typer.echo(ctx.get_help())
87
202
  raise typer.Exit()
88
203
 
89
- try:
90
- # Instantiate the main CLI class
91
- cli_instance = CLI(verbose=verbose)
204
+ # Use build-in role for --shell or --code mode
205
+ if role and role != DefaultRoleNames.DEFAULT and (shell or code):
206
+ print("Warning: --role is ignored when --shell or --code is used.")
207
+ role = DefaultRoleNames.DEFAULT
92
208
 
93
- # Handle list_chats option
94
- if list_chats:
95
- cli_instance._list_chats()
96
- return
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)
97
215
 
98
216
  # Run the appropriate mode
99
- cli_instance.run(chat=chat, shell=shell, prompt=final_prompt)
217
+ cli_instance.run(
218
+ chat=chat,
219
+ shell=shell,
220
+ input=final_prompt,
221
+ role=role,
222
+ )
100
223
  except Exception as e:
101
224
  # Catch potential errors during CLI initialization or run
102
225
  print(f"An error occurred: {e}")
yaicli/exceptions.py ADDED
@@ -0,0 +1,46 @@
1
+ class YAICLIException(Exception):
2
+ """Base exception for YAICLI"""
3
+
4
+ def __init__(self, message: str):
5
+ self.message = message
6
+ super().__init__(self.message)
7
+
8
+
9
+ ###################################
10
+ # Role exceptions
11
+ class RoleNotFoundError(YAICLIException):
12
+ """Exception raised when a role is not found"""
13
+
14
+ pass
15
+
16
+
17
+ class RoleAlreadyExistsError(YAICLIException):
18
+ """Exception raised when a role already exists"""
19
+
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