yaicli 0.3.2__py3-none-any.whl → 0.4.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.
- pyproject.toml +3 -2
- yaicli/cli.py +8 -8
- yaicli/config.py +6 -1
- yaicli/const.py +19 -20
- yaicli/providers/__init__.py +34 -0
- yaicli/providers/base.py +51 -0
- yaicli/providers/cohere.py +136 -0
- yaicli/providers/openai.py +176 -0
- yaicli/roles.py +42 -14
- {yaicli-0.3.2.dist-info → yaicli-0.4.0.dist-info}/METADATA +39 -76
- yaicli-0.4.0.dist-info/RECORD +23 -0
- yaicli/api.py +0 -316
- yaicli-0.3.2.dist-info/RECORD +0 -20
- {yaicli-0.3.2.dist-info → yaicli-0.4.0.dist-info}/WHEEL +0 -0
- {yaicli-0.3.2.dist-info → yaicli-0.4.0.dist-info}/entry_points.txt +0 -0
- {yaicli-0.3.2.dist-info → yaicli-0.4.0.dist-info}/licenses/LICENSE +0 -0
pyproject.toml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "yaicli"
|
3
|
-
version = "0.
|
3
|
+
version = "0.4.0"
|
4
4
|
description = "A simple CLI tool to interact with LLM"
|
5
5
|
authors = [{ name = "belingud", email = "im.victor@qq.com" }]
|
6
6
|
readme = "README.md"
|
@@ -30,9 +30,10 @@ keywords = [
|
|
30
30
|
"interact with llms",
|
31
31
|
]
|
32
32
|
dependencies = [
|
33
|
+
"cohere>=5.15.0",
|
33
34
|
"distro>=1.9.0",
|
34
35
|
"httpx>=0.28.1",
|
35
|
-
"
|
36
|
+
"openai>=1.76.0",
|
36
37
|
"prompt-toolkit>=3.0.50",
|
37
38
|
"rich>=13.9.4",
|
38
39
|
"socksio>=1.0.0",
|
yaicli/cli.py
CHANGED
@@ -16,7 +16,6 @@ from rich.padding import Padding
|
|
16
16
|
from rich.panel import Panel
|
17
17
|
from rich.prompt import Prompt
|
18
18
|
|
19
|
-
from yaicli.api import ApiClient
|
20
19
|
from yaicli.chat_manager import ChatFileInfo, ChatManager, FileChatManager
|
21
20
|
from yaicli.config import CONFIG_PATH, Config, cfg
|
22
21
|
from yaicli.console import get_console
|
@@ -40,6 +39,7 @@ from yaicli.const import (
|
|
40
39
|
)
|
41
40
|
from yaicli.history import LimitedFileHistory
|
42
41
|
from yaicli.printer import Printer
|
42
|
+
from yaicli.providers import BaseClient, create_api_client
|
43
43
|
from yaicli.roles import RoleManager
|
44
44
|
from yaicli.utils import detect_os, detect_shell, filter_command
|
45
45
|
|
@@ -51,7 +51,7 @@ class CLI:
|
|
51
51
|
self,
|
52
52
|
verbose: bool = False,
|
53
53
|
stdin: Optional[str] = None,
|
54
|
-
api_client: Optional[
|
54
|
+
api_client: Optional[BaseClient] = None,
|
55
55
|
printer: Optional[Printer] = None,
|
56
56
|
chat_manager: Optional[ChatManager] = None,
|
57
57
|
role: Optional[str] = None,
|
@@ -64,9 +64,10 @@ class CLI:
|
|
64
64
|
self.config: Config = cfg
|
65
65
|
self.current_mode: str = TEMP_MODE
|
66
66
|
self.role: str = role or DefaultRoleNames.DEFAULT.value
|
67
|
+
self.init_role: str = self.role # --role can specify a role when enter interactive chat
|
67
68
|
|
68
69
|
# Initialize role manager
|
69
|
-
self.role_manager = RoleManager()
|
70
|
+
self.role_manager = RoleManager() # Singleton
|
70
71
|
|
71
72
|
# Validate role
|
72
73
|
if not self.role_manager.role_exists(self.role):
|
@@ -102,7 +103,7 @@ class CLI:
|
|
102
103
|
self.console.print(f"Current role: {self.role}")
|
103
104
|
self.console.print(Markdown("---", code_theme=self.config.get("CODE_THEME", DEFAULT_CODE_THEME)))
|
104
105
|
|
105
|
-
self.api_client = api_client or
|
106
|
+
self.api_client = api_client or create_api_client(self.config, self.console, self.verbose)
|
106
107
|
self.printer = printer or Printer(self.config, self.console, self.verbose, markdown=True)
|
107
108
|
|
108
109
|
_origin_stderr = None
|
@@ -344,12 +345,11 @@ class CLI:
|
|
344
345
|
def get_system_prompt(self) -> str:
|
345
346
|
"""Get the system prompt based on current role and mode"""
|
346
347
|
# Use the role manager to get the system prompt
|
347
|
-
self.console.print(f"Using role: {self.role}")
|
348
348
|
return self.role_manager.get_system_prompt(self.role)
|
349
349
|
|
350
350
|
def _build_messages(self, user_input: str) -> List[dict]:
|
351
351
|
"""Build message list for LLM API"""
|
352
|
-
# Create the message list
|
352
|
+
# Create the message list with system prompt
|
353
353
|
messages = [{"role": "system", "content": self.get_system_prompt()}]
|
354
354
|
|
355
355
|
# Add previous conversation if available
|
@@ -420,7 +420,7 @@ class CLI:
|
|
420
420
|
████ ███████ ██ ██ ██ ██
|
421
421
|
██ ██ ██ ██ ██ ██ ██
|
422
422
|
██ ██ ██ ██ ██████ ███████ ██
|
423
|
-
|
423
|
+
""",
|
424
424
|
style="bold cyan",
|
425
425
|
)
|
426
426
|
self.console.print("Welcome to YAICLI!", style="bold")
|
@@ -500,7 +500,7 @@ class CLI:
|
|
500
500
|
@self.bindings.add(Keys.ControlI) # TAB
|
501
501
|
def _(event: KeyPressEvent) -> None:
|
502
502
|
self.current_mode = EXEC_MODE if self.current_mode == CHAT_MODE else CHAT_MODE
|
503
|
-
self.role = DefaultRoleNames.SHELL if self.current_mode == EXEC_MODE else
|
503
|
+
self.role = DefaultRoleNames.SHELL if self.current_mode == EXEC_MODE else self.init_role
|
504
504
|
|
505
505
|
def _run_once(self, input: str, shell: bool) -> None:
|
506
506
|
"""Run a single command (non-interactive)."""
|
yaicli/config.py
CHANGED
@@ -13,6 +13,7 @@ from yaicli.const import (
|
|
13
13
|
DEFAULT_CONFIG_MAP,
|
14
14
|
DEFAULT_JUSTIFY,
|
15
15
|
DEFAULT_MAX_SAVED_CHATS,
|
16
|
+
DEFAULT_ROLE_MODIFY_WARNING,
|
16
17
|
)
|
17
18
|
from yaicli.utils import str2bool
|
18
19
|
|
@@ -78,6 +79,10 @@ class Config(dict):
|
|
78
79
|
f.write(f"\nMAX_SAVED_CHATS={DEFAULT_MAX_SAVED_CHATS}\n")
|
79
80
|
if "JUSTIFY" not in config_content.strip():
|
80
81
|
f.write(f"\nJUSTIFY={DEFAULT_JUSTIFY}\n")
|
82
|
+
if "ROLE_MODIFY_WARNING" not in config_content.strip():
|
83
|
+
f.write(
|
84
|
+
f"\n# Set to false to disable warnings about modified built-in roles\nROLE_MODIFY_WARNING={DEFAULT_ROLE_MODIFY_WARNING}\n"
|
85
|
+
)
|
81
86
|
|
82
87
|
def _load_from_file(self) -> None:
|
83
88
|
"""Load configuration from the config file.
|
@@ -160,7 +165,7 @@ class Config(dict):
|
|
160
165
|
self[key] = converted_value
|
161
166
|
|
162
167
|
|
163
|
-
@lru_cache(
|
168
|
+
@lru_cache(1)
|
164
169
|
def get_config() -> Config:
|
165
170
|
"""Get the configuration singleton"""
|
166
171
|
return Config()
|
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
|
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"
|
@@ -33,25 +35,24 @@ ROLES_DIR = CONFIG_PATH.parent / "roles"
|
|
33
35
|
|
34
36
|
# Default configuration values
|
35
37
|
DEFAULT_CODE_THEME = "monokai"
|
36
|
-
DEFAULT_COMPLETION_PATH = "chat/completions"
|
37
|
-
DEFAULT_ANSWER_PATH = "choices[0].message.content"
|
38
38
|
DEFAULT_PROVIDER = "openai"
|
39
39
|
DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
40
40
|
DEFAULT_MODEL = "gpt-4o"
|
41
41
|
DEFAULT_SHELL_NAME = "auto"
|
42
42
|
DEFAULT_OS_NAME = "auto"
|
43
|
-
DEFAULT_STREAM = "true"
|
43
|
+
DEFAULT_STREAM: BOOL_STR = "true"
|
44
44
|
DEFAULT_TEMPERATURE: float = 0.7
|
45
45
|
DEFAULT_TOP_P: float = 1.0
|
46
46
|
DEFAULT_MAX_TOKENS: int = 1024
|
47
47
|
DEFAULT_MAX_HISTORY: int = 500
|
48
|
-
DEFAULT_AUTO_SUGGEST = "true"
|
49
|
-
DEFAULT_SHOW_REASONING = "true"
|
48
|
+
DEFAULT_AUTO_SUGGEST: BOOL_STR = "true"
|
49
|
+
DEFAULT_SHOW_REASONING: BOOL_STR = "true"
|
50
50
|
DEFAULT_TIMEOUT: int = 60
|
51
51
|
DEFAULT_INTERACTIVE_ROUND: int = 25
|
52
|
-
DEFAULT_CHAT_HISTORY_DIR = Path(gettempdir()) / "yaicli/chats"
|
52
|
+
DEFAULT_CHAT_HISTORY_DIR: Path = Path(gettempdir()) / "yaicli/chats"
|
53
53
|
DEFAULT_MAX_SAVED_CHATS = 20
|
54
54
|
DEFAULT_JUSTIFY: JustifyMethod = "default"
|
55
|
+
DEFAULT_ROLE_MODIFY_WARNING: BOOL_STR = "true"
|
55
56
|
|
56
57
|
|
57
58
|
class EventTypeEnum(StrEnum):
|
@@ -64,12 +65,11 @@ class EventTypeEnum(StrEnum):
|
|
64
65
|
FINISH = "finish"
|
65
66
|
|
66
67
|
|
67
|
-
SHELL_PROMPT = """
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
Supply plain text only, avoiding Markdown formatting."""
|
68
|
+
SHELL_PROMPT = """You are YAICLI, a shell command generator.
|
69
|
+
The context conversation may contain other types of messages,
|
70
|
+
but you should only respond with a single valid {_shell} shell command for {_os}.
|
71
|
+
Do not include any explanations, comments, or formatting — only the command as plain text, avoiding Markdown formatting.
|
72
|
+
"""
|
73
73
|
|
74
74
|
DEFAULT_PROMPT = """
|
75
75
|
You are YAICLI, a system management and programing assistant,
|
@@ -113,9 +113,6 @@ DEFAULT_CONFIG_MAP = {
|
|
113
113
|
# System detection hints
|
114
114
|
"SHELL_NAME": {"value": DEFAULT_SHELL_NAME, "env_key": "YAI_SHELL_NAME", "type": str},
|
115
115
|
"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
116
|
# API call parameters
|
120
117
|
"STREAM": {"value": DEFAULT_STREAM, "env_key": "YAI_STREAM", "type": bool},
|
121
118
|
"TEMPERATURE": {"value": DEFAULT_TEMPERATURE, "env_key": "YAI_TEMPERATURE", "type": float},
|
@@ -136,6 +133,8 @@ DEFAULT_CONFIG_MAP = {
|
|
136
133
|
# Chat history settings
|
137
134
|
"CHAT_HISTORY_DIR": {"value": DEFAULT_CHAT_HISTORY_DIR, "env_key": "YAI_CHAT_HISTORY_DIR", "type": str},
|
138
135
|
"MAX_SAVED_CHATS": {"value": DEFAULT_MAX_SAVED_CHATS, "env_key": "YAI_MAX_SAVED_CHATS", "type": int},
|
136
|
+
# Role settings
|
137
|
+
"ROLE_MODIFY_WARNING": {"value": DEFAULT_ROLE_MODIFY_WARNING, "env_key": "YAI_ROLE_MODIFY_WARNING", "type": bool},
|
139
138
|
}
|
140
139
|
|
141
140
|
DEFAULT_CONFIG_INI = f"""[core]
|
@@ -148,10 +147,6 @@ MODEL={DEFAULT_CONFIG_MAP["MODEL"]["value"]}
|
|
148
147
|
SHELL_NAME={DEFAULT_CONFIG_MAP["SHELL_NAME"]["value"]}
|
149
148
|
OS_NAME={DEFAULT_CONFIG_MAP["OS_NAME"]["value"]}
|
150
149
|
|
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
150
|
# true: streaming response, false: non-streaming
|
156
151
|
STREAM={DEFAULT_CONFIG_MAP["STREAM"]["value"]}
|
157
152
|
|
@@ -177,4 +172,8 @@ JUSTIFY={DEFAULT_CONFIG_MAP["JUSTIFY"]["value"]}
|
|
177
172
|
# Chat history settings
|
178
173
|
CHAT_HISTORY_DIR={DEFAULT_CONFIG_MAP["CHAT_HISTORY_DIR"]["value"]}
|
179
174
|
MAX_SAVED_CHATS={DEFAULT_CONFIG_MAP["MAX_SAVED_CHATS"]["value"]}
|
175
|
+
|
176
|
+
# Role settings
|
177
|
+
# Set to false to disable warnings about modified built-in roles
|
178
|
+
ROLE_MODIFY_WARNING={DEFAULT_CONFIG_MAP["ROLE_MODIFY_WARNING"]["value"]}
|
180
179
|
"""
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from yaicli.const import DEFAULT_PROVIDER
|
2
|
+
from yaicli.providers.base import BaseClient
|
3
|
+
from yaicli.providers.cohere import CohereClient
|
4
|
+
from yaicli.providers.openai import OpenAIClient
|
5
|
+
|
6
|
+
|
7
|
+
def create_api_client(config, console, verbose):
|
8
|
+
"""Factory function to create the appropriate API client based on provider.
|
9
|
+
|
10
|
+
Args:
|
11
|
+
config: The configuration dictionary
|
12
|
+
console: The rich console for output
|
13
|
+
verbose: Whether to enable verbose output
|
14
|
+
|
15
|
+
Returns:
|
16
|
+
An instance of the appropriate ApiClient implementation
|
17
|
+
"""
|
18
|
+
provider = config.get("PROVIDER", DEFAULT_PROVIDER).lower()
|
19
|
+
|
20
|
+
if provider == "openai":
|
21
|
+
return OpenAIClient(config, console, verbose)
|
22
|
+
elif provider == "cohere":
|
23
|
+
return CohereClient(config, console, verbose)
|
24
|
+
# elif provider == "google":
|
25
|
+
# return GoogleApiClient(config, console, verbose)
|
26
|
+
# elif provider == "claude":
|
27
|
+
# return ClaudeApiClient(config, console, verbose)
|
28
|
+
else:
|
29
|
+
# Fallback to openai client
|
30
|
+
console.print(f"Using generic HTTP client for provider: {provider}", style="yellow")
|
31
|
+
return OpenAIClient(config, console, verbose)
|
32
|
+
|
33
|
+
|
34
|
+
__all__ = ["BaseClient", "OpenAIClient", "CohereClient", "create_api_client"]
|
yaicli/providers/base.py
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
3
|
+
|
4
|
+
from rich.console import Console
|
5
|
+
|
6
|
+
|
7
|
+
class BaseClient(ABC):
|
8
|
+
"""Base abstract class for LLM API clients."""
|
9
|
+
|
10
|
+
def __init__(self, config: Dict[str, Any], console: Console, verbose: bool):
|
11
|
+
"""Initialize the API client with configuration."""
|
12
|
+
self.config = config
|
13
|
+
self.console = console
|
14
|
+
self.verbose = verbose
|
15
|
+
self.timeout = self.config["TIMEOUT"]
|
16
|
+
|
17
|
+
@abstractmethod
|
18
|
+
def completion(self, messages: List[Dict[str, str]]) -> Tuple[Optional[str], Optional[str]]:
|
19
|
+
"""Get a complete non-streamed response from the API."""
|
20
|
+
pass
|
21
|
+
|
22
|
+
@abstractmethod
|
23
|
+
def stream_completion(self, messages: List[Dict[str, str]]) -> Iterator[Dict[str, Any]]:
|
24
|
+
"""Connect to the API and yield parsed stream events."""
|
25
|
+
pass
|
26
|
+
|
27
|
+
def _get_reasoning_content(self, delta: dict) -> Optional[str]:
|
28
|
+
"""Extract reasoning content from delta if available based on specific keys.
|
29
|
+
|
30
|
+
This method checks for various keys that might contain reasoning content
|
31
|
+
in different API implementations.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
delta: The delta dictionary from the API response
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
The reasoning content string if found, None otherwise
|
38
|
+
"""
|
39
|
+
if not delta:
|
40
|
+
return None
|
41
|
+
# Reasoning content keys from API:
|
42
|
+
# reasoning_content: deepseek/infi-ai
|
43
|
+
# reasoning: openrouter
|
44
|
+
# <think> block implementation not in here
|
45
|
+
for key in ("reasoning_content", "reasoning"):
|
46
|
+
# Check if the key exists and its value is a non-empty string
|
47
|
+
value = delta.get(key)
|
48
|
+
if isinstance(value, str) and value:
|
49
|
+
return value
|
50
|
+
|
51
|
+
return None # Return None if no relevant key with a string value is found
|
@@ -0,0 +1,136 @@
|
|
1
|
+
from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple, TypeVar
|
2
|
+
|
3
|
+
from cohere import ChatResponse, ClientV2, StreamedChatResponseV2
|
4
|
+
from cohere.core.api_error import ApiError
|
5
|
+
|
6
|
+
from yaicli.const import EventTypeEnum
|
7
|
+
from yaicli.providers.base import BaseClient
|
8
|
+
|
9
|
+
ChunkType = Literal[
|
10
|
+
"message-start",
|
11
|
+
"content-start",
|
12
|
+
"content-delta",
|
13
|
+
"content-end",
|
14
|
+
"tool-plan-delta",
|
15
|
+
"tool-call-start",
|
16
|
+
"tool-call-delta",
|
17
|
+
"tool-call-end",
|
18
|
+
"citation-start",
|
19
|
+
"citation-end",
|
20
|
+
"message-end",
|
21
|
+
"debug",
|
22
|
+
]
|
23
|
+
|
24
|
+
# Type variable for chunks that have delta attribute
|
25
|
+
T = TypeVar("T", bound=StreamedChatResponseV2)
|
26
|
+
|
27
|
+
|
28
|
+
class CohereClient(BaseClient):
|
29
|
+
"""Cohere API client implementation using the official Cohere Python library."""
|
30
|
+
|
31
|
+
def __init__(self, config: Dict[str, Any], console, verbose: bool):
|
32
|
+
"""Initialize the Cohere API client with configuration."""
|
33
|
+
super().__init__(config, console, verbose)
|
34
|
+
self.api_key = config["API_KEY"]
|
35
|
+
self.model = config["MODEL"]
|
36
|
+
if not config["BASE_URL"] or "cohere" not in config["BASE_URL"]:
|
37
|
+
# BASE_URL can be empty, in which case we use the default base_url
|
38
|
+
self.base_url = "https://api.cohere.com"
|
39
|
+
else:
|
40
|
+
self.base_url = config["BASE_URL"]
|
41
|
+
self.base_url = self.base_url.rstrip("/")
|
42
|
+
if self.base_url.endswith("v2") or self.base_url.endswith("v1"):
|
43
|
+
self.base_url = self.base_url[:-2]
|
44
|
+
|
45
|
+
# Initialize the Cohere client with our custom configuration
|
46
|
+
self.client = ClientV2(
|
47
|
+
api_key=self.api_key,
|
48
|
+
base_url=self.base_url,
|
49
|
+
client_name="Yaicli",
|
50
|
+
timeout=self.timeout,
|
51
|
+
)
|
52
|
+
|
53
|
+
def _prepare_request_params(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
|
54
|
+
"""Prepare the common request parameters for Cohere API calls."""
|
55
|
+
# P value must be between 0.01 and 0.99, default to 0.75 if outside this range, also cohere api default is 0.75
|
56
|
+
p = 0.75 if not (0.01 < self.config["TOP_P"] < 0.99) else self.config["TOP_P"]
|
57
|
+
return {
|
58
|
+
"messages": messages,
|
59
|
+
"model": self.model,
|
60
|
+
"temperature": self.config["TEMPERATURE"],
|
61
|
+
"max_tokens": self.config["MAX_TOKENS"],
|
62
|
+
"p": p,
|
63
|
+
}
|
64
|
+
|
65
|
+
def _process_completion_response(self, response: ChatResponse) -> Tuple[Optional[str], Optional[str]]:
|
66
|
+
"""Process the response from a non-streamed Cohere completion request."""
|
67
|
+
try:
|
68
|
+
content = response.message.content
|
69
|
+
if not content:
|
70
|
+
return None, None
|
71
|
+
text = content[0].text
|
72
|
+
if not text:
|
73
|
+
return None, None
|
74
|
+
return text, None
|
75
|
+
|
76
|
+
except Exception as e:
|
77
|
+
self.console.print(f"Error processing Cohere response: {e}", style="red")
|
78
|
+
if self.verbose:
|
79
|
+
self.console.print(f"Response: {response}")
|
80
|
+
return None, None
|
81
|
+
|
82
|
+
def completion(self, messages: List[Dict[str, str]]) -> Tuple[Optional[str], Optional[str]]:
|
83
|
+
"""Get a complete non-streamed response from the Cohere API."""
|
84
|
+
params = self._prepare_request_params(messages)
|
85
|
+
|
86
|
+
try:
|
87
|
+
response: ChatResponse = self.client.chat(**params)
|
88
|
+
return self._process_completion_response(response)
|
89
|
+
except ApiError as e:
|
90
|
+
self.console.print(f"Cohere API error: {e}", style="red")
|
91
|
+
if self.verbose:
|
92
|
+
self.console.print(f"Response: {e.body}")
|
93
|
+
return None, None
|
94
|
+
|
95
|
+
def stream_completion(self, messages: List[Dict[str, str]]) -> Iterator[Dict[str, Any]]:
|
96
|
+
"""Connect to the Cohere API and yield parsed stream events."""
|
97
|
+
params = self._prepare_request_params(messages)
|
98
|
+
|
99
|
+
try:
|
100
|
+
for chunk in self.client.v2.chat_stream(**params):
|
101
|
+
# Skip message start/end events
|
102
|
+
if chunk.type in ("message-start", "message-end", "content-end"): # type: ignore
|
103
|
+
continue
|
104
|
+
|
105
|
+
# Safe attribute checking - skip if any required attribute is missing
|
106
|
+
if not hasattr(chunk, "delta"):
|
107
|
+
continue
|
108
|
+
|
109
|
+
# At this point we know chunk has delta attribute
|
110
|
+
delta = getattr(chunk, "delta")
|
111
|
+
if delta is None or not hasattr(delta, "message"):
|
112
|
+
continue
|
113
|
+
|
114
|
+
message = getattr(delta, "message")
|
115
|
+
if message is None or not hasattr(message, "content"):
|
116
|
+
continue
|
117
|
+
|
118
|
+
content = getattr(message, "content")
|
119
|
+
if content is None or not hasattr(content, "text"):
|
120
|
+
continue
|
121
|
+
|
122
|
+
# Access text safely
|
123
|
+
text = getattr(content, "text")
|
124
|
+
if text:
|
125
|
+
yield {"type": EventTypeEnum.CONTENT, "chunk": text}
|
126
|
+
|
127
|
+
except ApiError as e:
|
128
|
+
self.console.print(f"Cohere API error during streaming: {e}", style="red")
|
129
|
+
yield {"type": EventTypeEnum.ERROR, "message": str(e)}
|
130
|
+
except Exception as e:
|
131
|
+
self.console.print(f"Unexpected error during Cohere streaming: {e}", style="red")
|
132
|
+
if self.verbose:
|
133
|
+
import traceback
|
134
|
+
|
135
|
+
traceback.print_exc()
|
136
|
+
yield {"type": EventTypeEnum.ERROR, "message": f"Unexpected stream error: {e}"}
|
@@ -0,0 +1,176 @@
|
|
1
|
+
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
2
|
+
|
3
|
+
import openai
|
4
|
+
from openai import OpenAI
|
5
|
+
from openai.types.chat.chat_completion import ChatCompletion
|
6
|
+
from openai.types.chat.chat_completion_chunk import Choice, ChoiceDelta
|
7
|
+
|
8
|
+
from yaicli.const import EventTypeEnum
|
9
|
+
from yaicli.providers.base import BaseClient
|
10
|
+
|
11
|
+
|
12
|
+
class OpenAIClient(BaseClient):
|
13
|
+
"""OpenAI API client implementation using the official OpenAI Python library."""
|
14
|
+
|
15
|
+
def __init__(self, config: Dict[str, Any], console, verbose: bool):
|
16
|
+
"""Initialize the OpenAI API client with configuration."""
|
17
|
+
super().__init__(config, console, verbose)
|
18
|
+
self.api_key = config["API_KEY"]
|
19
|
+
self.model = config["MODEL"]
|
20
|
+
self.base_url = config["BASE_URL"]
|
21
|
+
|
22
|
+
# Initialize the OpenAI client with our custom configuration
|
23
|
+
self.client = OpenAI(
|
24
|
+
api_key=self.api_key,
|
25
|
+
base_url=self.base_url,
|
26
|
+
timeout=self.timeout,
|
27
|
+
default_headers={"X-Title": "Yaicli"},
|
28
|
+
max_retries=2, # Add retry logic for resilience
|
29
|
+
)
|
30
|
+
|
31
|
+
def _prepare_request_params(self, messages: List[Dict[str, str]], stream: bool) -> Dict[str, Any]:
|
32
|
+
"""Prepare the common request parameters for OpenAI API calls."""
|
33
|
+
return {
|
34
|
+
"messages": messages,
|
35
|
+
"model": self.model,
|
36
|
+
"stream": stream,
|
37
|
+
"temperature": self.config["TEMPERATURE"],
|
38
|
+
"top_p": self.config["TOP_P"],
|
39
|
+
# Openai: This value is now deprecated in favor of max_completion_tokens
|
40
|
+
"max_tokens": self.config["MAX_TOKENS"],
|
41
|
+
"max_completion_tokens": self.config["MAX_TOKENS"],
|
42
|
+
}
|
43
|
+
|
44
|
+
def _process_completion_response(self, conpletion: ChatCompletion) -> Tuple[Optional[str], Optional[str]]:
|
45
|
+
"""Process the response from a non-streamed OpenAI completion request."""
|
46
|
+
try:
|
47
|
+
# OpenAI SDK returns structured objects
|
48
|
+
content = conpletion.choices[0].message.content
|
49
|
+
reasoning = None
|
50
|
+
|
51
|
+
# Check for reasoning in model_extra
|
52
|
+
if hasattr(conpletion.choices[0].message, "model_extra") and conpletion.choices[0].message.model_extra:
|
53
|
+
extra = conpletion.choices[0].message.model_extra
|
54
|
+
if extra and "reasoning" in extra:
|
55
|
+
reasoning = extra["reasoning"]
|
56
|
+
|
57
|
+
# If no reasoning in model_extra, try extracting from <think> tags
|
58
|
+
if reasoning is None and isinstance(content, str):
|
59
|
+
content = content.lstrip()
|
60
|
+
if content.startswith("<think>"):
|
61
|
+
think_end = content.find("</think>")
|
62
|
+
if think_end != -1:
|
63
|
+
reasoning = content[7:think_end].strip() # Start after <think>
|
64
|
+
# Remove the <think> block from the main content
|
65
|
+
content = content[think_end + 8 :].strip() # Start after </think>
|
66
|
+
|
67
|
+
return content, reasoning
|
68
|
+
except Exception as e:
|
69
|
+
self.console.print(f"Error processing OpenAI response: {e}", style="red")
|
70
|
+
if self.verbose:
|
71
|
+
self.console.print(f"Response: {conpletion}")
|
72
|
+
return None, None
|
73
|
+
|
74
|
+
def completion(self, messages: List[Dict[str, str]]) -> Tuple[Optional[str], Optional[str]]:
|
75
|
+
"""Get a complete non-streamed response from the OpenAI API."""
|
76
|
+
params = self._prepare_request_params(messages, stream=False)
|
77
|
+
|
78
|
+
try:
|
79
|
+
# Use context manager for proper resource management
|
80
|
+
with self.client.with_options(timeout=self.timeout) as client:
|
81
|
+
response: ChatCompletion = client.chat.completions.create(**params)
|
82
|
+
return self._process_completion_response(response)
|
83
|
+
except openai.APIConnectionError as e:
|
84
|
+
self.console.print(f"OpenAI connection error: {e}", style="red")
|
85
|
+
if self.verbose:
|
86
|
+
self.console.print(f"Underlying error: {e.__cause__}")
|
87
|
+
return None, None
|
88
|
+
except openai.RateLimitError as e:
|
89
|
+
self.console.print(f"OpenAI rate limit error (429): {e}", style="red")
|
90
|
+
return None, None
|
91
|
+
except openai.APIStatusError as e:
|
92
|
+
self.console.print(f"OpenAI API error (status {e.status_code}): {e}", style="red")
|
93
|
+
if self.verbose:
|
94
|
+
self.console.print(f"Response: {e.response}")
|
95
|
+
return None, None
|
96
|
+
except Exception as e:
|
97
|
+
self.console.print(f"Unexpected error during OpenAI completion: {e}", style="red")
|
98
|
+
if self.verbose:
|
99
|
+
import traceback
|
100
|
+
|
101
|
+
traceback.print_exc()
|
102
|
+
return None, None
|
103
|
+
|
104
|
+
def stream_completion(self, messages: List[Dict[str, str]]) -> Iterator[Dict[str, Any]]:
|
105
|
+
"""Connect to the OpenAI API and yield parsed stream events.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
messages: The list of message dictionaries to send to the API
|
109
|
+
|
110
|
+
Yields:
|
111
|
+
Event dictionaries with the following structure:
|
112
|
+
- type: The event type (from EventTypeEnum)
|
113
|
+
- chunk/message/reason: The content of the event
|
114
|
+
"""
|
115
|
+
params: Dict[str, Any] = self._prepare_request_params(messages, stream=True)
|
116
|
+
in_reasoning: bool = False
|
117
|
+
|
118
|
+
try:
|
119
|
+
# Use context manager to ensure proper cleanup
|
120
|
+
with self.client.chat.completions.create(**params) as stream:
|
121
|
+
for chunk in stream:
|
122
|
+
choices: List[Choice] = chunk.choices
|
123
|
+
if not choices:
|
124
|
+
# Some APIs may return empty choices upon reaching the end of content.
|
125
|
+
continue
|
126
|
+
choice: Choice = choices[0]
|
127
|
+
delta: ChoiceDelta = choice.delta
|
128
|
+
finish_reason: Optional[str] = choice.finish_reason
|
129
|
+
|
130
|
+
# Process model_extra for reasoning content
|
131
|
+
if hasattr(delta, "model_extra") and delta.model_extra:
|
132
|
+
reasoning: Optional[str] = self._get_reasoning_content(delta.model_extra)
|
133
|
+
if reasoning:
|
134
|
+
yield {"type": EventTypeEnum.REASONING, "chunk": reasoning}
|
135
|
+
in_reasoning = True
|
136
|
+
continue
|
137
|
+
|
138
|
+
# Process content delta
|
139
|
+
if hasattr(delta, "content") and delta.content:
|
140
|
+
content_chunk = delta.content
|
141
|
+
if in_reasoning and content_chunk:
|
142
|
+
# Send reasoning end signal before content
|
143
|
+
in_reasoning = False
|
144
|
+
yield {"type": EventTypeEnum.REASONING_END, "chunk": ""}
|
145
|
+
yield {"type": EventTypeEnum.CONTENT, "chunk": content_chunk}
|
146
|
+
elif content_chunk:
|
147
|
+
yield {"type": EventTypeEnum.CONTENT, "chunk": content_chunk}
|
148
|
+
|
149
|
+
# Process finish reason
|
150
|
+
if finish_reason:
|
151
|
+
# Send reasoning end signal if still in reasoning state
|
152
|
+
if in_reasoning:
|
153
|
+
in_reasoning = False
|
154
|
+
yield {"type": EventTypeEnum.REASONING_END, "chunk": ""}
|
155
|
+
yield {"type": EventTypeEnum.FINISH, "reason": finish_reason}
|
156
|
+
|
157
|
+
except openai.APIConnectionError as e:
|
158
|
+
self.console.print(f"OpenAI connection error during streaming: {e}", style="red")
|
159
|
+
if self.verbose:
|
160
|
+
self.console.print(f"Underlying error: {e.__cause__}")
|
161
|
+
yield {"type": EventTypeEnum.ERROR, "message": str(e)}
|
162
|
+
except openai.RateLimitError as e:
|
163
|
+
self.console.print(f"OpenAI rate limit error (429) during streaming: {e}", style="red")
|
164
|
+
yield {"type": EventTypeEnum.ERROR, "message": str(e)}
|
165
|
+
except openai.APIStatusError as e:
|
166
|
+
self.console.print(f"OpenAI API error (status {e.status_code}) during streaming: {e}", style="red")
|
167
|
+
if self.verbose:
|
168
|
+
self.console.print(f"Response: {e.response}")
|
169
|
+
yield {"type": EventTypeEnum.ERROR, "message": str(e)}
|
170
|
+
except Exception as e:
|
171
|
+
self.console.print(f"Unexpected error during OpenAI streaming: {e}", style="red")
|
172
|
+
if self.verbose:
|
173
|
+
import traceback
|
174
|
+
|
175
|
+
traceback.print_exc()
|
176
|
+
yield {"type": EventTypeEnum.ERROR, "message": f"Unexpected stream error: {e}"}
|
yaicli/roles.py
CHANGED
@@ -19,19 +19,19 @@ class Role:
|
|
19
19
|
self, name: str, prompt: str, variables: Optional[Dict[str, Any]] = None, filepath: Optional[str] = None
|
20
20
|
):
|
21
21
|
self.name = name
|
22
|
-
self.
|
22
|
+
self._prompt = prompt
|
23
23
|
if not variables:
|
24
24
|
variables = {"_os": detect_os(cfg), "_shell": detect_shell(cfg)}
|
25
25
|
self.variables = variables
|
26
26
|
self.filepath = filepath
|
27
27
|
|
28
|
-
self.prompt = self.
|
28
|
+
self.prompt = self._prompt.format(**self.variables)
|
29
29
|
|
30
30
|
def to_dict(self) -> Dict[str, Any]:
|
31
31
|
"""Convert Role to dictionary for serialization"""
|
32
32
|
return {
|
33
33
|
"name": self.name,
|
34
|
-
"prompt": self.
|
34
|
+
"prompt": self._prompt,
|
35
35
|
}
|
36
36
|
|
37
37
|
@classmethod
|
@@ -51,13 +51,23 @@ class Role:
|
|
51
51
|
class RoleManager:
|
52
52
|
roles_dir: Path = ROLES_DIR
|
53
53
|
console: Console = get_console()
|
54
|
+
_roles: Optional[Dict[str, Role]] = None
|
54
55
|
|
55
|
-
def
|
56
|
-
|
56
|
+
def __new__(cls):
|
57
|
+
"""Singleton class for RoleManager"""
|
58
|
+
if not hasattr(cls, "instance"):
|
59
|
+
cls.instance = super().__new__(cls)
|
60
|
+
return cls.instance
|
61
|
+
|
62
|
+
@property
|
63
|
+
def roles(self) -> Dict[str, Role]:
|
64
|
+
if self._roles is None:
|
65
|
+
self._roles = self._load_roles()
|
66
|
+
return self._roles
|
57
67
|
|
58
68
|
def _load_roles(self) -> Dict[str, Role]:
|
59
69
|
"""Load all role configurations"""
|
60
|
-
roles = {}
|
70
|
+
roles: Dict[str, Role] = {}
|
61
71
|
self.roles_dir.mkdir(parents=True, exist_ok=True)
|
62
72
|
|
63
73
|
# Check if any role files exist
|
@@ -87,11 +97,34 @@ class RoleManager:
|
|
87
97
|
# Ensure default roles exist
|
88
98
|
for role_id, role_config in DEFAULT_ROLES.items():
|
89
99
|
if role_id not in roles:
|
100
|
+
# Default role doesn't exist locally, create it
|
90
101
|
role_file = self.roles_dir / f"{role_id}.json"
|
91
102
|
filepath = str(role_file)
|
92
103
|
roles[role_id] = Role.from_dict(role_id, role_config, filepath)
|
93
104
|
with role_file.open("w", encoding="utf-8") as f:
|
94
105
|
json.dump(role_config, f, indent=2)
|
106
|
+
else:
|
107
|
+
# TODO: Maybe not necessary
|
108
|
+
# Check if the local role's prompt differs from the built-in one
|
109
|
+
local_role = roles[role_id]
|
110
|
+
builtin_prompt = role_config.get("prompt", "")
|
111
|
+
|
112
|
+
# Only show warning if ROLE_MODIFY_WARNING is enabled
|
113
|
+
if cfg["ROLE_MODIFY_WARNING"]:
|
114
|
+
if local_role._prompt != builtin_prompt:
|
115
|
+
self.console.print(
|
116
|
+
f"[yellow]Warning:[/yellow] Local role '{role_id}' has a different prompt than the built-in role.",
|
117
|
+
style="yellow",
|
118
|
+
)
|
119
|
+
self.console.print(
|
120
|
+
"To reset to the built-in version, delete the local role file with: "
|
121
|
+
f'[bold]ai --delete-role "{role_id}"[/bold]',
|
122
|
+
style="dim",
|
123
|
+
)
|
124
|
+
self.console.print(
|
125
|
+
"To disable this warning, set [bold]ROLE_MODIFY_WARNING=false[/bold] in your config file.",
|
126
|
+
style="dim",
|
127
|
+
)
|
95
128
|
|
96
129
|
return roles
|
97
130
|
|
@@ -142,12 +175,12 @@ class RoleManager:
|
|
142
175
|
return self.roles.get(role_id)
|
143
176
|
|
144
177
|
@classmethod
|
145
|
-
def check_id_ok(cls, role_id: str):
|
178
|
+
def check_id_ok(cls, param: typer.CallbackParam, role_id: str):
|
146
179
|
"""Check if role exists by ID.
|
147
180
|
This method is a cli option callback.
|
148
181
|
If role does not exist, exit with error.
|
149
182
|
"""
|
150
|
-
if not role_id:
|
183
|
+
if not role_id or role_id == param.default:
|
151
184
|
return role_id
|
152
185
|
self = cls()
|
153
186
|
if not self.role_exists(role_id):
|
@@ -216,11 +249,6 @@ class RoleManager:
|
|
216
249
|
self.console.print(f"Role '{role_id}' does not exist", style="red")
|
217
250
|
return False
|
218
251
|
|
219
|
-
# Don't allow deleting default roles
|
220
|
-
if role_id in DEFAULT_ROLES:
|
221
|
-
self.console.print(f"Cannot delete default role: '{role_id}'", style="red")
|
222
|
-
return False
|
223
|
-
|
224
252
|
try:
|
225
253
|
role = self.roles[role_id]
|
226
254
|
if role.filepath:
|
@@ -244,5 +272,5 @@ class RoleManager:
|
|
244
272
|
role = Role.from_dict(DefaultRoleNames.DEFAULT, default_config)
|
245
273
|
|
246
274
|
# Create a copy of the role with system variables
|
247
|
-
system_role = Role(name=role.name, prompt=role.
|
275
|
+
system_role = Role(name=role.name, prompt=role._prompt)
|
248
276
|
return system_role.prompt
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: yaicli
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.4.0
|
4
4
|
Summary: A simple CLI tool to interact with LLM
|
5
5
|
Project-URL: Homepage, https://github.com/belingud/yaicli
|
6
6
|
Project-URL: Repository, https://github.com/belingud/yaicli
|
@@ -213,9 +213,10 @@ Classifier: License :: OSI Approved :: MIT License
|
|
213
213
|
Classifier: Operating System :: OS Independent
|
214
214
|
Classifier: Programming Language :: Python :: 3
|
215
215
|
Requires-Python: >=3.9
|
216
|
+
Requires-Dist: cohere>=5.15.0
|
216
217
|
Requires-Dist: distro>=1.9.0
|
217
218
|
Requires-Dist: httpx>=0.28.1
|
218
|
-
Requires-Dist:
|
219
|
+
Requires-Dist: openai>=1.76.0
|
219
220
|
Requires-Dist: prompt-toolkit>=3.0.50
|
220
221
|
Requires-Dist: rich>=13.9.4
|
221
222
|
Requires-Dist: socksio>=1.0.0
|
@@ -235,9 +236,7 @@ generate and execute shell commands, or get quick answers without leaving your w
|
|
235
236
|
|
236
237
|
**Supports both standard and deep reasoning models across all major LLM providers.**
|
237
238
|
|
238
|
-
<
|
239
|
-
<img src="https://vhs.charm.sh/vhs-5U1BBjJkTUBReRswsSgIVx.gif" alt="YAICLI Chat Demo" width="85%">
|
240
|
-
</p>
|
239
|
+
<a href="https://asciinema.org/a/vyreM0n576GjGL2asjI3QzUIY" target="_blank"><img src="https://asciinema.org/a/vyreM0n576GjGL2asjI3QzUIY.svg" width="85%"/></a>
|
241
240
|
|
242
241
|
> [!NOTE]
|
243
242
|
> YAICLI is actively developed. While core functionality is stable, some features may evolve in future releases.
|
@@ -329,10 +328,6 @@ MODEL = gpt-4o
|
|
329
328
|
SHELL_NAME = auto
|
330
329
|
OS_NAME = auto
|
331
330
|
|
332
|
-
# API paths (usually no need to change for OpenAI compatible APIs)
|
333
|
-
COMPLETION_PATH = chat/completions
|
334
|
-
ANSWER_PATH = choices[0].message.content
|
335
|
-
|
336
331
|
# true: streaming response, false: non-streaming
|
337
332
|
STREAM = true
|
338
333
|
|
@@ -362,29 +357,28 @@ MAX_SAVED_CHATS = 20
|
|
362
357
|
|
363
358
|
### Configuration Options Reference
|
364
359
|
|
365
|
-
| Option
|
366
|
-
|
367
|
-
| `PROVIDER`
|
368
|
-
| `BASE_URL`
|
369
|
-
| `API_KEY`
|
370
|
-
| `MODEL`
|
371
|
-
| `SHELL_NAME`
|
372
|
-
| `OS_NAME`
|
373
|
-
| `
|
374
|
-
| `
|
375
|
-
| `
|
376
|
-
| `
|
377
|
-
| `
|
378
|
-
| `
|
379
|
-
| `
|
380
|
-
| `
|
381
|
-
| `
|
382
|
-
| `
|
383
|
-
| `
|
384
|
-
| `
|
385
|
-
| `
|
386
|
-
| `
|
387
|
-
| `MAX_SAVED_CHATS` | Max saved chats | `20` | `YAI_MAX_SAVED_CHATS` |
|
360
|
+
| Option | Description | Default | Env Variable |
|
361
|
+
| --------------------- | ------------------------------------------- | --------------------------- | ------------------------- |
|
362
|
+
| `PROVIDER` | LLM provider (openai, claude, cohere, etc.) | `openai` | `YAI_PROVIDER` |
|
363
|
+
| `BASE_URL` | API endpoint URL | `https://api.openai.com/v1` | `YAI_BASE_URL` |
|
364
|
+
| `API_KEY` | Your API key | - | `YAI_API_KEY` |
|
365
|
+
| `MODEL` | LLM model to use | `gpt-4o` | `YAI_MODEL` |
|
366
|
+
| `SHELL_NAME` | Shell type | `auto` | `YAI_SHELL_NAME` |
|
367
|
+
| `OS_NAME` | Operating system | `auto` | `YAI_OS_NAME` |
|
368
|
+
| `STREAM` | Enable streaming | `true` | `YAI_STREAM` |
|
369
|
+
| `TIMEOUT` | API timeout (seconds) | `60` | `YAI_TIMEOUT` |
|
370
|
+
| `INTERACTIVE_ROUND` | Interactive mode rounds | `25` | `YAI_INTERACTIVE_ROUND` |
|
371
|
+
| `CODE_THEME` | Syntax highlighting theme | `monokai` | `YAI_CODE_THEME` |
|
372
|
+
| `TEMPERATURE` | Response randomness | `0.7` | `YAI_TEMPERATURE` |
|
373
|
+
| `TOP_P` | Top-p sampling | `1.0` | `YAI_TOP_P` |
|
374
|
+
| `MAX_TOKENS` | Max response tokens | `1024` | `YAI_MAX_TOKENS` |
|
375
|
+
| `MAX_HISTORY` | Max history entries | `500` | `YAI_MAX_HISTORY` |
|
376
|
+
| `AUTO_SUGGEST` | Enable history suggestions | `true` | `YAI_AUTO_SUGGEST` |
|
377
|
+
| `SHOW_REASONING` | Enable reasoning display | `true` | `YAI_SHOW_REASONING` |
|
378
|
+
| `JUSTIFY` | Text alignment | `default` | `YAI_JUSTIFY` |
|
379
|
+
| `CHAT_HISTORY_DIR` | Chat history directory | `<tempdir>/yaicli/history` | `YAI_CHAT_HISTORY_DIR` |
|
380
|
+
| `MAX_SAVED_CHATS` | Max saved chats | `20` | `YAI_MAX_SAVED_CHATS` |
|
381
|
+
| `ROLE_MODIFY_WARNING` | Warn user when modifying role | `true` | `YAI_ROLE_MODIFY_WARNING` |
|
388
382
|
|
389
383
|
### LLM Provider Configuration
|
390
384
|
|
@@ -393,52 +387,23 @@ other providers.
|
|
393
387
|
|
394
388
|
#### Pre-configured Provider Settings
|
395
389
|
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
|
401
|
-
|
|
402
|
-
| **
|
390
|
+
`provider` is not case sensitive.
|
391
|
+
|
392
|
+
Claude and gemini native api will support soon.
|
393
|
+
|
394
|
+
| Provider | BASE_URL |
|
395
|
+
| ------------------------------ | --------------------------------------------------------- |
|
396
|
+
| **OpenAI** (default) | `https://api.openai.com/v1` |
|
397
|
+
| **Claude** (native API) | `https://api.anthropic.com/v1` |
|
398
|
+
| **Claude** (OpenAI-compatible) | `https://api.anthropic.com/v1/openai` |
|
399
|
+
| **Cohere** | `https://api.cohere.com` |
|
400
|
+
| **Gemini** | `https://generativelanguage.googleapis.com/v1beta/openai` |
|
403
401
|
|
404
402
|
> **Note**: Many providers offer OpenAI-compatible endpoints that work with the default settings.
|
405
403
|
>
|
406
404
|
> - Google Gemini: https://ai.google.dev/gemini-api/docs/openai
|
407
405
|
> - Claude: https://docs.anthropic.com/en/api/openai-sdk
|
408
406
|
|
409
|
-
#### Custom Provider Configuration Guide
|
410
|
-
|
411
|
-
To configure a custom provider:
|
412
|
-
|
413
|
-
1. **Find the API Endpoint**:
|
414
|
-
|
415
|
-
- Check the provider's API documentation for their chat completion endpoint
|
416
|
-
|
417
|
-
2. **Identify the Response Structure**:
|
418
|
-
|
419
|
-
- Look at the JSON response format to find where the text content is located
|
420
|
-
|
421
|
-
3. **Set the Path Expression**:
|
422
|
-
- Use jmespath syntax to specify the path to the text content
|
423
|
-
|
424
|
-
**Example**: For Claude's native API, the response looks like:
|
425
|
-
|
426
|
-
```json
|
427
|
-
{
|
428
|
-
"content": [
|
429
|
-
{
|
430
|
-
"text": "Hi! My name is Claude.",
|
431
|
-
"type": "text"
|
432
|
-
}
|
433
|
-
],
|
434
|
-
"id": "msg_013Zva2CMHLNnXjNJJKqJ2EF",
|
435
|
-
"model": "claude-3-7-sonnet-20250219",
|
436
|
-
"role": "assistant"
|
437
|
-
}
|
438
|
-
```
|
439
|
-
|
440
|
-
The path to extract the text is: `content.[0].text`
|
441
|
-
|
442
407
|
### Syntax Highlighting Themes
|
443
408
|
|
444
409
|
YAICLI supports all Pygments syntax highlighting themes. You can set your preferred theme in the config file:
|
@@ -449,7 +414,7 @@ CODE_THEME = monokai
|
|
449
414
|
|
450
415
|
Browse available themes at: https://pygments.org/styles/
|
451
416
|
|
452
|
-

|
453
418
|
|
454
419
|
## 🚀 Usage
|
455
420
|
|
@@ -849,12 +814,10 @@ YAICLI is designed with a modular architecture that separates concerns and makes
|
|
849
814
|
### Dependencies
|
850
815
|
|
851
816
|
| Library | Purpose |
|
852
|
-
|
817
|
+
| --------------------------------------------------------------- | -------------------------------------------------- |
|
853
818
|
| [Typer](https://typer.tiangolo.com/) | Command-line interface with type hints |
|
854
819
|
| [Rich](https://rich.readthedocs.io/) | Terminal formatting and beautiful display |
|
855
820
|
| [prompt_toolkit](https://python-prompt-toolkit.readthedocs.io/) | Interactive input with history and auto-completion |
|
856
|
-
| [httpx](https://www.python-httpx.org/) | Modern HTTP client with async support |
|
857
|
-
| [jmespath](https://jmespath.org/) | JSON data extraction |
|
858
821
|
|
859
822
|
## 👨💻 Contributing
|
860
823
|
|
@@ -0,0 +1,23 @@
|
|
1
|
+
pyproject.toml,sha256=qBb73j9dLZ13fGtQDN81nxfqfpUVs5HNDq5hyfwxOtQ,1540
|
2
|
+
yaicli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
yaicli/chat_manager.py,sha256=I7BAMz91FLYT6x69wbomtAGLx0WsoTwS4Wo0MgP6P9I,10644
|
4
|
+
yaicli/cli.py,sha256=kqUXSDQej4lMefI2QQpM-Do01Be6CUphh65_Nu7sDck,22948
|
5
|
+
yaicli/config.py,sha256=nfnrMz_Q78oHqDVvWSrQRGFyBl-q5MScvnYlQRWDQ_g,6515
|
6
|
+
yaicli/console.py,sha256=291F4hGksJtxYpg_mehepCIJ-eB2MaDNIyv1JAMgJ1Y,1985
|
7
|
+
yaicli/const.py,sha256=WjNzJAP37XizJYyd-LBTWxZAfJ2Nj7mj-eECPMOr3Do,6927
|
8
|
+
yaicli/entry.py,sha256=Yp0Z--x-7dowrz-h8hJJ4_BoCzuDjS11NcM8YgFzUoY,7460
|
9
|
+
yaicli/exceptions.py,sha256=ndedSdE0uaxxHrWN944BkbhMfRMSMxGDfmqmCKCGJco,924
|
10
|
+
yaicli/history.py,sha256=s-57X9FMsaQHF7XySq1gGH_jpd_cHHTYafYu2ECuG6M,2472
|
11
|
+
yaicli/printer.py,sha256=nXpralD5qZJQga3OTdEPhj22g7UoF-4mJbZeOtWXojo,12430
|
12
|
+
yaicli/render.py,sha256=mB1OT9859_PTwI9f-KY802lPaeQXKRw6ls_5jN21jWc,511
|
13
|
+
yaicli/roles.py,sha256=LdPqpdBKQr_eX7PV7iwRtFT0_trmWXVvZUmb3OMhK0w,10582
|
14
|
+
yaicli/utils.py,sha256=MLvb-C5n19AD9Z1nW4Z3Z43ZKNH8STxQmNDnL7mq26E,4490
|
15
|
+
yaicli/providers/__init__.py,sha256=zYXcnV8HdNeLEPRJEeSgSboNuphkdA1PrbB8MkDErAk,1243
|
16
|
+
yaicli/providers/base.py,sha256=Q6QoxrIVqDYcUp1-FY5O-LT33rKSb1jeXGnrbd4Xlog,1870
|
17
|
+
yaicli/providers/cohere.py,sha256=m_HIHm6NEd1VG4wCmDKYn3KHo2jzC98FsfxRzjWR8-M,5411
|
18
|
+
yaicli/providers/openai.py,sha256=CKNN3wDsnCEOwZAMrARi43-AV65WgJ7AvHONgvpLZw8,8541
|
19
|
+
yaicli-0.4.0.dist-info/METADATA,sha256=oeusTm4BZ1Dvow0IF_c2urp_onnV41OgE7QzwcuP09c,43917
|
20
|
+
yaicli-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
21
|
+
yaicli-0.4.0.dist-info/entry_points.txt,sha256=iMhGm3btBaqrknQoF6WCg5sdx69ZyNSC73tRpCcbcLw,63
|
22
|
+
yaicli-0.4.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
23
|
+
yaicli-0.4.0.dist-info/RECORD,,
|
yaicli/api.py
DELETED
@@ -1,316 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
3
|
-
|
4
|
-
import httpx
|
5
|
-
import jmespath
|
6
|
-
from rich.console import Console
|
7
|
-
|
8
|
-
from yaicli.const import (
|
9
|
-
DEFAULT_BASE_URL,
|
10
|
-
DEFAULT_COMPLETION_PATH,
|
11
|
-
DEFAULT_MODEL,
|
12
|
-
EventTypeEnum,
|
13
|
-
)
|
14
|
-
|
15
|
-
|
16
|
-
def parse_stream_line(line: Union[bytes, str], console: Console, verbose: bool) -> Optional[dict]:
|
17
|
-
"""(Helper Function) Parse a single line from the SSE stream response."""
|
18
|
-
if not isinstance(line, (bytes, str)):
|
19
|
-
if verbose:
|
20
|
-
console.print(f"Warning: Received non-string/bytes line: {line!r}", style="yellow")
|
21
|
-
return None
|
22
|
-
line_str: str = line.decode("utf-8") if isinstance(line, bytes) else line
|
23
|
-
line_str = line_str.strip()
|
24
|
-
if not line_str or not line_str.startswith("data: "):
|
25
|
-
return None
|
26
|
-
|
27
|
-
data_part = line_str[6:]
|
28
|
-
if data_part.lower() == "[done]":
|
29
|
-
return {"done": True} # Use a specific dictionary to signal DONE
|
30
|
-
|
31
|
-
try:
|
32
|
-
json_data = json.loads(data_part)
|
33
|
-
if not isinstance(json_data, dict) or "choices" not in json_data:
|
34
|
-
if verbose:
|
35
|
-
console.print(f"Warning: Invalid stream data format (missing 'choices'): {data_part}", style="yellow")
|
36
|
-
return None
|
37
|
-
return json_data
|
38
|
-
except json.JSONDecodeError:
|
39
|
-
console.print("Error decoding response JSON", style="red")
|
40
|
-
if verbose:
|
41
|
-
console.print(f"Invalid JSON data: {data_part}", style="red")
|
42
|
-
return None
|
43
|
-
|
44
|
-
|
45
|
-
class ApiClient:
|
46
|
-
"""Handles communication with the LLM API."""
|
47
|
-
|
48
|
-
def __init__(self, config: Dict[str, Any], console: Console, verbose: bool, client: Optional[httpx.Client] = None):
|
49
|
-
"""Initialize the API client with configuration."""
|
50
|
-
self.config = config
|
51
|
-
self.console = console
|
52
|
-
self.verbose = verbose
|
53
|
-
self.base_url = str(config.get("BASE_URL", DEFAULT_BASE_URL))
|
54
|
-
self.completion_path = str(config.get("COMPLETION_PATH", DEFAULT_COMPLETION_PATH))
|
55
|
-
self.api_key = str(config.get("API_KEY", ""))
|
56
|
-
self.model = str(config.get("MODEL", DEFAULT_MODEL))
|
57
|
-
self.timeout = self.config["TIMEOUT"]
|
58
|
-
self.client = client or httpx.Client(timeout=self.config["TIMEOUT"])
|
59
|
-
|
60
|
-
def _prepare_request_body(self, messages: List[Dict[str, str]], stream: bool) -> Dict[str, Any]:
|
61
|
-
"""Prepare the common request body for API calls."""
|
62
|
-
return {
|
63
|
-
"messages": messages,
|
64
|
-
"model": self.model,
|
65
|
-
"stream": stream,
|
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"],
|
72
|
-
}
|
73
|
-
|
74
|
-
def _handle_api_error(self, e: httpx.HTTPError) -> None:
|
75
|
-
"""Handle and print HTTP errors consistently."""
|
76
|
-
if isinstance(e, httpx.TimeoutException):
|
77
|
-
self.console.print(f"Error: API request timed out after {self.timeout} seconds. {e}", style="red")
|
78
|
-
elif isinstance(e, httpx.HTTPStatusError):
|
79
|
-
self.console.print(f"Error calling API: {e.response.status_code} {e.response.reason_phrase}", style="red")
|
80
|
-
if self.verbose:
|
81
|
-
self.console.print(f"Response Text: {e.response.text}")
|
82
|
-
elif isinstance(e, httpx.RequestError):
|
83
|
-
api_url = self.get_completion_url()
|
84
|
-
self.console.print(f"Error: Could not connect to API endpoint '{api_url}'. {e}", style="red")
|
85
|
-
else:
|
86
|
-
self.console.print(f"An unexpected HTTP error occurred: {e}", style="red")
|
87
|
-
|
88
|
-
def get_completion_url(self) -> str:
|
89
|
-
"""Get the full completion URL."""
|
90
|
-
base_url = self.base_url.rstrip("/")
|
91
|
-
completion_path = self.completion_path.lstrip("/")
|
92
|
-
return f"{base_url}/{completion_path}"
|
93
|
-
|
94
|
-
def get_headers(self) -> Dict[str, str]:
|
95
|
-
"""Get the request headers."""
|
96
|
-
return {
|
97
|
-
"Authorization": f"Bearer {self.api_key}",
|
98
|
-
"Content-Type": "application/json",
|
99
|
-
"X-Title": "Yaicli",
|
100
|
-
}
|
101
|
-
|
102
|
-
def _process_completion_response(self, response_json: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
|
103
|
-
"""Process the JSON response from a non-streamed completion request."""
|
104
|
-
answer_path = self.config["ANSWER_PATH"]
|
105
|
-
message_path = answer_path.rsplit(".", 1)[0]
|
106
|
-
|
107
|
-
# Extract content and reasoning using JMESPath
|
108
|
-
content = jmespath.search(answer_path, response_json)
|
109
|
-
message = jmespath.search(message_path, response_json)
|
110
|
-
reasoning = self._get_reasoning_content(
|
111
|
-
message
|
112
|
-
) # Reuse reasoning extraction if applicable to the whole message
|
113
|
-
|
114
|
-
# Process string content and extract reasoning from <think> tags if present
|
115
|
-
if isinstance(content, str):
|
116
|
-
content = content.lstrip()
|
117
|
-
if content.startswith("<think>"):
|
118
|
-
think_end = content.find("</think>")
|
119
|
-
if think_end != -1:
|
120
|
-
# Extract reasoning from <think> tag only if not already found via message path
|
121
|
-
if reasoning is None:
|
122
|
-
reasoning = content[7:think_end].strip() # Start after <think>
|
123
|
-
# Remove the <think> block from the main content
|
124
|
-
content = content[think_end + 8 :].strip() # Start after </think>
|
125
|
-
# If it doesn't start with <think>, or if </think> wasn't found, return content as is
|
126
|
-
return content, reasoning
|
127
|
-
elif content:
|
128
|
-
self.console.print(
|
129
|
-
f"Warning: Unexpected content type from API: {type(content)}. Path: {answer_path}", style="yellow"
|
130
|
-
)
|
131
|
-
# Attempt to convert unexpected content to string, return existing reasoning
|
132
|
-
return str(content), reasoning
|
133
|
-
else:
|
134
|
-
self.console.print(f"Warning: Could not extract content using JMESPath '{answer_path}'.", style="yellow")
|
135
|
-
if self.verbose:
|
136
|
-
self.console.print(f"API Response: {response_json}")
|
137
|
-
return None, reasoning
|
138
|
-
|
139
|
-
def completion(self, messages: List[Dict[str, str]]) -> Tuple[Optional[str], Optional[str]]:
|
140
|
-
"""Get a complete non-streamed response from the API."""
|
141
|
-
url = self.get_completion_url()
|
142
|
-
body = self._prepare_request_body(messages, stream=False)
|
143
|
-
headers = self.get_headers()
|
144
|
-
|
145
|
-
try:
|
146
|
-
response = self.client.post(url, json=body, headers=headers)
|
147
|
-
response.raise_for_status()
|
148
|
-
response_json = response.json()
|
149
|
-
# Delegate processing to the helper method
|
150
|
-
return self._process_completion_response(response_json)
|
151
|
-
|
152
|
-
except httpx.HTTPError as e:
|
153
|
-
self._handle_api_error(e)
|
154
|
-
return None, None
|
155
|
-
|
156
|
-
def _handle_http_error(self, e: httpx.HTTPStatusError) -> Dict[str, Any]:
|
157
|
-
"""Handle HTTP errors during streaming and return an error event.
|
158
|
-
|
159
|
-
Args:
|
160
|
-
e: The HTTP status error that occurred
|
161
|
-
|
162
|
-
Returns:
|
163
|
-
An error event dictionary to be yielded to the client
|
164
|
-
"""
|
165
|
-
error_body = e.response.read()
|
166
|
-
self._handle_api_error(e)
|
167
|
-
|
168
|
-
try:
|
169
|
-
error_json = json.loads(error_body)
|
170
|
-
error_message = error_json.get("error", {}).get("message")
|
171
|
-
except (json.JSONDecodeError, AttributeError):
|
172
|
-
error_message = None
|
173
|
-
|
174
|
-
if not error_message:
|
175
|
-
error_message = error_body.decode() if error_body else str(e)
|
176
|
-
|
177
|
-
return {"type": EventTypeEnum.ERROR, "message": error_message}
|
178
|
-
|
179
|
-
def _process_stream_chunk(
|
180
|
-
self, parsed_data: Dict[str, Any], in_reasoning: bool
|
181
|
-
) -> Iterator[Tuple[Dict[str, Any], bool]]:
|
182
|
-
"""Process a single chunk from the stream and yield events with updated reasoning state.
|
183
|
-
|
184
|
-
Args:
|
185
|
-
parsed_data: The parsed JSON data from a streamline
|
186
|
-
in_reasoning: Whether we're currently in a reasoning state
|
187
|
-
|
188
|
-
Yields:
|
189
|
-
A tuple containing:
|
190
|
-
- An event dictionary to yield to the client
|
191
|
-
- The updated reasoning state
|
192
|
-
"""
|
193
|
-
# Handle stream errors
|
194
|
-
if "error" in parsed_data:
|
195
|
-
error_msg = parsed_data["error"].get("message", "Unknown error in stream data")
|
196
|
-
self.console.print(f"Error in stream data: {error_msg}", style="red")
|
197
|
-
yield {"type": EventTypeEnum.ERROR, "message": error_msg}, in_reasoning
|
198
|
-
return
|
199
|
-
|
200
|
-
# Get and validate the choice
|
201
|
-
choices = parsed_data.get("choices", [])
|
202
|
-
if not choices or not isinstance(choices, list):
|
203
|
-
if self.verbose:
|
204
|
-
self.console.print(f"Skipping stream chunk with no choices: {parsed_data}", style="dim")
|
205
|
-
return
|
206
|
-
|
207
|
-
choice = choices[0]
|
208
|
-
if not isinstance(choice, dict):
|
209
|
-
if self.verbose:
|
210
|
-
self.console.print(f"Skipping stream chunk with invalid choice structure: {choice}", style="dim")
|
211
|
-
return
|
212
|
-
|
213
|
-
# Get content from delta
|
214
|
-
delta = choice.get("delta", {})
|
215
|
-
if not isinstance(delta, dict):
|
216
|
-
if self.verbose:
|
217
|
-
self.console.print(f"Skipping stream chunk with invalid delta structure: {delta}", style="dim")
|
218
|
-
return
|
219
|
-
|
220
|
-
# Process content
|
221
|
-
reason = self._get_reasoning_content(delta)
|
222
|
-
content_chunk = delta.get("content", "")
|
223
|
-
finish_reason = choice.get("finish_reason")
|
224
|
-
|
225
|
-
# Yield events based on content type
|
226
|
-
if reason is not None:
|
227
|
-
in_reasoning = True
|
228
|
-
yield {"type": EventTypeEnum.REASONING, "chunk": reason}, in_reasoning
|
229
|
-
elif in_reasoning and content_chunk and isinstance(content_chunk, str):
|
230
|
-
# Signal the end of reasoning before yielding content
|
231
|
-
in_reasoning = False
|
232
|
-
yield {"type": EventTypeEnum.REASONING_END, "chunk": ""}, in_reasoning
|
233
|
-
yield {"type": EventTypeEnum.CONTENT, "chunk": content_chunk}, in_reasoning
|
234
|
-
elif content_chunk and isinstance(content_chunk, str):
|
235
|
-
yield {"type": EventTypeEnum.CONTENT, "chunk": content_chunk}, in_reasoning
|
236
|
-
|
237
|
-
if finish_reason:
|
238
|
-
yield {"type": EventTypeEnum.FINISH, "reason": finish_reason}, in_reasoning
|
239
|
-
|
240
|
-
def stream_completion(self, messages: List[Dict[str, str]]) -> Iterator[Dict[str, Any]]:
|
241
|
-
"""Connect to the API and yield parsed stream events.
|
242
|
-
|
243
|
-
This method handles the streaming API connection and processes the response,
|
244
|
-
yielding events that can be consumed by the client. It handles various types
|
245
|
-
of content including regular content and reasoning content.
|
246
|
-
|
247
|
-
Args:
|
248
|
-
messages: The list of message dictionaries to send to the API
|
249
|
-
|
250
|
-
Yields:
|
251
|
-
Event dictionaries with the following structure:
|
252
|
-
- type: The event type (from EventTypeEnum)
|
253
|
-
- chunk/message/reason: The content of the event
|
254
|
-
"""
|
255
|
-
url = self.get_completion_url()
|
256
|
-
body = self._prepare_request_body(messages, stream=True)
|
257
|
-
headers = self.get_headers()
|
258
|
-
in_reasoning = False
|
259
|
-
|
260
|
-
try:
|
261
|
-
with self.client.stream("POST", url, json=body, headers=headers) as response:
|
262
|
-
try:
|
263
|
-
response.raise_for_status()
|
264
|
-
except httpx.HTTPStatusError as e:
|
265
|
-
yield self._handle_http_error(e)
|
266
|
-
return
|
267
|
-
|
268
|
-
# Process the streamline by line
|
269
|
-
for line in response.iter_lines():
|
270
|
-
parsed_data = parse_stream_line(line, self.console, self.verbose)
|
271
|
-
if parsed_data is None:
|
272
|
-
continue
|
273
|
-
if parsed_data.get("done"):
|
274
|
-
break
|
275
|
-
|
276
|
-
# Process chunks and yield events
|
277
|
-
for event, updated_state in self._process_stream_chunk(parsed_data, in_reasoning):
|
278
|
-
in_reasoning = updated_state
|
279
|
-
# event: {type: str, Optional[chunk]: str, Optional[message]: str, Optional[reason]: str}
|
280
|
-
yield event
|
281
|
-
|
282
|
-
except httpx.HTTPError as e:
|
283
|
-
self._handle_api_error(e)
|
284
|
-
yield {"type": EventTypeEnum.ERROR, "message": str(e)}
|
285
|
-
except Exception as e:
|
286
|
-
self.console.print(f"An unexpected error occurred during streaming: {e}", style="red")
|
287
|
-
if self.verbose:
|
288
|
-
import traceback
|
289
|
-
|
290
|
-
traceback.print_exc()
|
291
|
-
yield {"type": EventTypeEnum.ERROR, "message": f"Unexpected stream error: {e}"}
|
292
|
-
|
293
|
-
def _get_reasoning_content(self, delta: dict) -> Optional[str]:
|
294
|
-
"""Extract reasoning content from delta if available based on specific keys.
|
295
|
-
|
296
|
-
This method checks for various keys that might contain reasoning content
|
297
|
-
in different API implementations.
|
298
|
-
|
299
|
-
Args:
|
300
|
-
delta: The delta dictionary from the API response
|
301
|
-
|
302
|
-
Returns:
|
303
|
-
The reasoning content string if found, None otherwise
|
304
|
-
"""
|
305
|
-
if not delta:
|
306
|
-
return None
|
307
|
-
# reasoning_content: deepseek/infi-ai
|
308
|
-
# reasoning: openrouter
|
309
|
-
# <think> block implementation not in here
|
310
|
-
for key in ("reasoning_content", "reasoning"):
|
311
|
-
# Check if the key exists and its value is a non-empty string
|
312
|
-
value = delta.get(key)
|
313
|
-
if isinstance(value, str) and value:
|
314
|
-
return value
|
315
|
-
|
316
|
-
return None # Return None if no relevant key with a string value is found
|
yaicli-0.3.2.dist-info/RECORD
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
pyproject.toml,sha256=C7FTGTVlazP67RAED5c9JjzINHUfFe7xcu05LrZv1mk,1519
|
2
|
-
yaicli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
yaicli/api.py,sha256=9kRozzxBKduQsda3acnxzvOD9wRLL0cH182L4ddHY8E,13666
|
4
|
-
yaicli/chat_manager.py,sha256=I7BAMz91FLYT6x69wbomtAGLx0WsoTwS4Wo0MgP6P9I,10644
|
5
|
-
yaicli/cli.py,sha256=z5t4XxKcl4MRGMTaASr0bc9PiidPpoSaBsgJd6d69-Y,22850
|
6
|
-
yaicli/config.py,sha256=xtzgXApM93zCqSUxmVSBdph0co_NKfEUU3hWtPe8qvM,6236
|
7
|
-
yaicli/console.py,sha256=291F4hGksJtxYpg_mehepCIJ-eB2MaDNIyv1JAMgJ1Y,1985
|
8
|
-
yaicli/const.py,sha256=iOQNG6M4EBmKgbwZdNqRsHrcQ7Od1nKOyLAqUhfMEBM,7020
|
9
|
-
yaicli/entry.py,sha256=Yp0Z--x-7dowrz-h8hJJ4_BoCzuDjS11NcM8YgFzUoY,7460
|
10
|
-
yaicli/exceptions.py,sha256=ndedSdE0uaxxHrWN944BkbhMfRMSMxGDfmqmCKCGJco,924
|
11
|
-
yaicli/history.py,sha256=s-57X9FMsaQHF7XySq1gGH_jpd_cHHTYafYu2ECuG6M,2472
|
12
|
-
yaicli/printer.py,sha256=nXpralD5qZJQga3OTdEPhj22g7UoF-4mJbZeOtWXojo,12430
|
13
|
-
yaicli/render.py,sha256=mB1OT9859_PTwI9f-KY802lPaeQXKRw6ls_5jN21jWc,511
|
14
|
-
yaicli/roles.py,sha256=bhXpLnGTPRZp3-K1Tt6ppTsuG2v9S0RAXikfMFhDs_U,9144
|
15
|
-
yaicli/utils.py,sha256=MLvb-C5n19AD9Z1nW4Z3Z43ZKNH8STxQmNDnL7mq26E,4490
|
16
|
-
yaicli-0.3.2.dist-info/METADATA,sha256=YHnAg8uWg41eBr1GwnMO4ch5S_sf1x2zdTnqlGQm-5g,45320
|
17
|
-
yaicli-0.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
18
|
-
yaicli-0.3.2.dist-info/entry_points.txt,sha256=iMhGm3btBaqrknQoF6WCg5sdx69ZyNSC73tRpCcbcLw,63
|
19
|
-
yaicli-0.3.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
20
|
-
yaicli-0.3.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|