shell-gpt-plus 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sgpt/__init__.py +5 -0
- sgpt/__main__.py +3 -0
- sgpt/__version__.py +1 -0
- sgpt/app.py +276 -0
- sgpt/cache.py +61 -0
- sgpt/config.py +92 -0
- sgpt/function.py +67 -0
- sgpt/functions/execute_shell.py +86 -0
- sgpt/functions/fetch_url.py +44 -0
- sgpt/functions/recall.py +39 -0
- sgpt/functions/remember.py +37 -0
- sgpt/functions/web_search.py +52 -0
- sgpt/handlers/__init__.py +0 -0
- sgpt/handlers/chat_handler.py +177 -0
- sgpt/handlers/default_handler.py +22 -0
- sgpt/handlers/handler.py +253 -0
- sgpt/handlers/repl_handler.py +66 -0
- sgpt/init_config.py +27 -0
- sgpt/integration.py +27 -0
- sgpt/llm_functions/__init__.py +0 -0
- sgpt/llm_functions/common/execute_shell.py +42 -0
- sgpt/llm_functions/init_functions.py +35 -0
- sgpt/llm_functions/mac/apple_script.py +47 -0
- sgpt/printer.py +65 -0
- sgpt/role.py +189 -0
- sgpt/roles/Code Generator.json +4 -0
- sgpt/roles/Shell Command Descriptor.json +4 -0
- sgpt/roles/Shell Command Generator.json +4 -0
- sgpt/roles/ShellGPT.json +4 -0
- sgpt/utils.py +95 -0
- shell_gpt_plus-1.1.0.dist-info/METADATA +601 -0
- shell_gpt_plus-1.1.0.dist-info/RECORD +35 -0
- shell_gpt_plus-1.1.0.dist-info/WHEEL +4 -0
- shell_gpt_plus-1.1.0.dist-info/entry_points.txt +2 -0
- shell_gpt_plus-1.1.0.dist-info/licenses/LICENSE +21 -0
sgpt/__init__.py
ADDED
sgpt/__main__.py
ADDED
sgpt/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.0"
|
sgpt/app.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
# To allow users to use arrow keys in the REPL.
|
|
4
|
+
import readline # noqa: F401
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from click import UsageError
|
|
9
|
+
from click.types import Choice
|
|
10
|
+
from prompt_toolkit import PromptSession
|
|
11
|
+
|
|
12
|
+
from sgpt.config import cfg
|
|
13
|
+
from sgpt.function import get_openai_schemas
|
|
14
|
+
from sgpt.handlers.chat_handler import ChatHandler
|
|
15
|
+
from sgpt.handlers.default_handler import DefaultHandler
|
|
16
|
+
from sgpt.handlers.repl_handler import ReplHandler
|
|
17
|
+
from sgpt.llm_functions.init_functions import install_functions as inst_funcs
|
|
18
|
+
from sgpt.role import DefaultRoles, SystemRole
|
|
19
|
+
from sgpt.utils import (
|
|
20
|
+
get_edited_prompt,
|
|
21
|
+
get_sgpt_version,
|
|
22
|
+
install_shell_integration,
|
|
23
|
+
run_command,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main(
|
|
28
|
+
prompt: str = typer.Argument(
|
|
29
|
+
"",
|
|
30
|
+
show_default=False,
|
|
31
|
+
help="The prompt to generate completions for.",
|
|
32
|
+
),
|
|
33
|
+
model: str = typer.Option(
|
|
34
|
+
cfg.get("DEFAULT_MODEL"),
|
|
35
|
+
help="Large language model to use.",
|
|
36
|
+
),
|
|
37
|
+
temperature: float = typer.Option(
|
|
38
|
+
0.0,
|
|
39
|
+
min=0.0,
|
|
40
|
+
max=2.0,
|
|
41
|
+
help="Randomness of generated output.",
|
|
42
|
+
),
|
|
43
|
+
top_p: float = typer.Option(
|
|
44
|
+
1.0,
|
|
45
|
+
min=0.0,
|
|
46
|
+
max=1.0,
|
|
47
|
+
help="Limits highest probable tokens (words).",
|
|
48
|
+
),
|
|
49
|
+
md: bool = typer.Option(
|
|
50
|
+
cfg.get("PRETTIFY_MARKDOWN") == "true",
|
|
51
|
+
help="Prettify markdown output.",
|
|
52
|
+
),
|
|
53
|
+
shell: bool = typer.Option(
|
|
54
|
+
False,
|
|
55
|
+
"--shell",
|
|
56
|
+
"-s",
|
|
57
|
+
help="Generate and execute shell commands.",
|
|
58
|
+
rich_help_panel="Assistance Options",
|
|
59
|
+
),
|
|
60
|
+
interaction: bool = typer.Option(
|
|
61
|
+
cfg.get("SHELL_INTERACTION") == "true",
|
|
62
|
+
help="Interactive mode for --shell option.",
|
|
63
|
+
rich_help_panel="Assistance Options",
|
|
64
|
+
),
|
|
65
|
+
describe_shell: bool = typer.Option(
|
|
66
|
+
False,
|
|
67
|
+
"--describe-shell",
|
|
68
|
+
"-d",
|
|
69
|
+
help="Describe a shell command.",
|
|
70
|
+
rich_help_panel="Assistance Options",
|
|
71
|
+
),
|
|
72
|
+
code: bool = typer.Option(
|
|
73
|
+
False,
|
|
74
|
+
"--code",
|
|
75
|
+
"-c",
|
|
76
|
+
help="Generate only code.",
|
|
77
|
+
rich_help_panel="Assistance Options",
|
|
78
|
+
),
|
|
79
|
+
functions: bool = typer.Option(
|
|
80
|
+
cfg.get("OPENAI_USE_FUNCTIONS") == "true",
|
|
81
|
+
help="Allow function calls.",
|
|
82
|
+
rich_help_panel="Assistance Options",
|
|
83
|
+
),
|
|
84
|
+
editor: bool = typer.Option(
|
|
85
|
+
False,
|
|
86
|
+
help="Open $EDITOR to provide a prompt.",
|
|
87
|
+
),
|
|
88
|
+
cache: bool = typer.Option(
|
|
89
|
+
True,
|
|
90
|
+
help="Cache completion results.",
|
|
91
|
+
),
|
|
92
|
+
version: bool = typer.Option(
|
|
93
|
+
False,
|
|
94
|
+
"--version",
|
|
95
|
+
help="Show version.",
|
|
96
|
+
callback=get_sgpt_version,
|
|
97
|
+
),
|
|
98
|
+
chat: str = typer.Option(
|
|
99
|
+
None,
|
|
100
|
+
help="Follow conversation with id, " 'use "temp" for quick session.',
|
|
101
|
+
rich_help_panel="Chat Options",
|
|
102
|
+
),
|
|
103
|
+
repl: str = typer.Option(
|
|
104
|
+
None,
|
|
105
|
+
help="Start a REPL (Read-eval-print loop) session.",
|
|
106
|
+
rich_help_panel="Chat Options",
|
|
107
|
+
),
|
|
108
|
+
show_chat: str = typer.Option(
|
|
109
|
+
None,
|
|
110
|
+
help="Show all messages from provided chat id.",
|
|
111
|
+
rich_help_panel="Chat Options",
|
|
112
|
+
),
|
|
113
|
+
list_chats: bool = typer.Option(
|
|
114
|
+
False,
|
|
115
|
+
"--list-chats",
|
|
116
|
+
"-lc",
|
|
117
|
+
help="List all existing chat ids.",
|
|
118
|
+
callback=ChatHandler.list_ids,
|
|
119
|
+
rich_help_panel="Chat Options",
|
|
120
|
+
),
|
|
121
|
+
role: str = typer.Option(
|
|
122
|
+
None,
|
|
123
|
+
help="System role for GPT model.",
|
|
124
|
+
rich_help_panel="Role Options",
|
|
125
|
+
),
|
|
126
|
+
create_role: str = typer.Option(
|
|
127
|
+
None,
|
|
128
|
+
help="Create role.",
|
|
129
|
+
callback=SystemRole.create,
|
|
130
|
+
rich_help_panel="Role Options",
|
|
131
|
+
),
|
|
132
|
+
show_role: str = typer.Option(
|
|
133
|
+
None,
|
|
134
|
+
help="Show role.",
|
|
135
|
+
callback=SystemRole.show,
|
|
136
|
+
rich_help_panel="Role Options",
|
|
137
|
+
),
|
|
138
|
+
list_roles: bool = typer.Option(
|
|
139
|
+
False,
|
|
140
|
+
"--list-roles",
|
|
141
|
+
"-lr",
|
|
142
|
+
help="List roles.",
|
|
143
|
+
callback=SystemRole.list,
|
|
144
|
+
rich_help_panel="Role Options",
|
|
145
|
+
),
|
|
146
|
+
install_integration: bool = typer.Option(
|
|
147
|
+
False,
|
|
148
|
+
help="Install shell integration (ZSH and Bash only)",
|
|
149
|
+
callback=install_shell_integration,
|
|
150
|
+
hidden=True, # Hiding since should be used only once.
|
|
151
|
+
),
|
|
152
|
+
install_functions: bool = typer.Option(
|
|
153
|
+
False,
|
|
154
|
+
help="Install default functions.",
|
|
155
|
+
callback=inst_funcs,
|
|
156
|
+
hidden=True, # Hiding since should be used only once.
|
|
157
|
+
),
|
|
158
|
+
) -> None:
|
|
159
|
+
stdin_passed = not sys.stdin.isatty()
|
|
160
|
+
|
|
161
|
+
if stdin_passed:
|
|
162
|
+
stdin = ""
|
|
163
|
+
# TODO: This is very hacky.
|
|
164
|
+
# In some cases, we need to pass stdin along with inputs.
|
|
165
|
+
# When we want part of stdin to be used as a init prompt,
|
|
166
|
+
# but rest of the stdin to be used as a inputs. For example:
|
|
167
|
+
# echo "hello\n__sgpt__eof__\nThis is input" | sgpt --repl temp
|
|
168
|
+
# In this case, "hello" will be used as a init prompt, and
|
|
169
|
+
# "This is input" will be used as "interactive" input to the REPL.
|
|
170
|
+
# This is useful to test REPL with some initial context.
|
|
171
|
+
for line in sys.stdin:
|
|
172
|
+
if "__sgpt__eof__" in line:
|
|
173
|
+
break
|
|
174
|
+
stdin += line
|
|
175
|
+
prompt = f"{stdin}\n\n{prompt}" if prompt else stdin
|
|
176
|
+
try:
|
|
177
|
+
# Switch to stdin for interactive input.
|
|
178
|
+
if os.name == "posix":
|
|
179
|
+
sys.stdin = open("/dev/tty", "r")
|
|
180
|
+
elif os.name == "nt":
|
|
181
|
+
sys.stdin = open("CON", "r")
|
|
182
|
+
except OSError:
|
|
183
|
+
# Non-interactive shell.
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
if show_chat:
|
|
187
|
+
ChatHandler.show_messages(show_chat, md)
|
|
188
|
+
|
|
189
|
+
if sum((shell, describe_shell, code)) > 1:
|
|
190
|
+
raise UsageError(
|
|
191
|
+
"Only one of --shell, --describe-shell, and --code options can be used at a time."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if chat and repl:
|
|
195
|
+
raise UsageError("--chat and --repl options cannot be used together.")
|
|
196
|
+
|
|
197
|
+
if editor and stdin_passed:
|
|
198
|
+
raise UsageError("--editor option cannot be used with stdin input.")
|
|
199
|
+
|
|
200
|
+
if editor:
|
|
201
|
+
prompt = get_edited_prompt()
|
|
202
|
+
|
|
203
|
+
role_class = (
|
|
204
|
+
DefaultRoles.check_get(shell, describe_shell, code)
|
|
205
|
+
if not role
|
|
206
|
+
else SystemRole.get(role)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
function_schemas = (get_openai_schemas() or None) if functions else None
|
|
210
|
+
|
|
211
|
+
if repl:
|
|
212
|
+
# Will be in infinite loop here until user exits with Ctrl+C.
|
|
213
|
+
ReplHandler(repl, role_class, md).handle(
|
|
214
|
+
init_prompt=prompt,
|
|
215
|
+
model=model,
|
|
216
|
+
temperature=temperature,
|
|
217
|
+
top_p=top_p,
|
|
218
|
+
caching=cache,
|
|
219
|
+
functions=function_schemas,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if chat:
|
|
223
|
+
full_completion = ChatHandler(chat, role_class, md).handle(
|
|
224
|
+
prompt=prompt,
|
|
225
|
+
model=model,
|
|
226
|
+
temperature=temperature,
|
|
227
|
+
top_p=top_p,
|
|
228
|
+
caching=cache,
|
|
229
|
+
functions=function_schemas,
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
full_completion = DefaultHandler(role_class, md).handle(
|
|
233
|
+
prompt=prompt,
|
|
234
|
+
model=model,
|
|
235
|
+
temperature=temperature,
|
|
236
|
+
top_p=top_p,
|
|
237
|
+
caching=cache,
|
|
238
|
+
functions=function_schemas,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
session: PromptSession[str] = PromptSession()
|
|
242
|
+
|
|
243
|
+
while shell and interaction:
|
|
244
|
+
option = typer.prompt(
|
|
245
|
+
text="[E]xecute, [M]odify, [D]escribe, [A]bort",
|
|
246
|
+
type=Choice(("e", "m", "d", "a", "y"), case_sensitive=False),
|
|
247
|
+
default="e" if cfg.get("DEFAULT_EXECUTE_SHELL_CMD") == "true" else "a",
|
|
248
|
+
show_choices=False,
|
|
249
|
+
show_default=False,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if option in ("e", "y"):
|
|
253
|
+
# "y" option is for keeping compatibility with old version.
|
|
254
|
+
run_command(full_completion)
|
|
255
|
+
elif option == "m":
|
|
256
|
+
full_completion = session.prompt("", default=full_completion)
|
|
257
|
+
continue
|
|
258
|
+
elif option == "d":
|
|
259
|
+
DefaultHandler(DefaultRoles.DESCRIBE_SHELL.get_role(), md).handle(
|
|
260
|
+
full_completion,
|
|
261
|
+
model=model,
|
|
262
|
+
temperature=temperature,
|
|
263
|
+
top_p=top_p,
|
|
264
|
+
caching=cache,
|
|
265
|
+
functions=function_schemas,
|
|
266
|
+
)
|
|
267
|
+
continue
|
|
268
|
+
break
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def entry_point() -> None:
|
|
272
|
+
typer.run(main)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
if __name__ == "__main__":
|
|
276
|
+
entry_point()
|
sgpt/cache.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from hashlib import md5
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Callable, Generator, no_type_check
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Cache:
|
|
8
|
+
"""
|
|
9
|
+
Decorator class that adds caching functionality to a function.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, length: int, cache_path: Path) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Initialize the Cache decorator.
|
|
15
|
+
|
|
16
|
+
:param length: Integer, maximum number of cache files to keep.
|
|
17
|
+
"""
|
|
18
|
+
self.length = length
|
|
19
|
+
self.cache_path = cache_path
|
|
20
|
+
self.cache_path.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
|
|
22
|
+
def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
|
|
23
|
+
"""
|
|
24
|
+
The Cache decorator.
|
|
25
|
+
|
|
26
|
+
:param func: The function to cache.
|
|
27
|
+
:return: Wrapped function with caching.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def wrapper(*args: Any, **kwargs: Any) -> Generator[str, None, None]:
|
|
31
|
+
key = md5(json.dumps((args[1:], kwargs)).encode("utf-8")).hexdigest()
|
|
32
|
+
file = self.cache_path / key
|
|
33
|
+
if kwargs.pop("caching") and file.exists():
|
|
34
|
+
yield file.read_text()
|
|
35
|
+
return
|
|
36
|
+
result = ""
|
|
37
|
+
for i in func(*args, **kwargs):
|
|
38
|
+
result += i
|
|
39
|
+
yield i
|
|
40
|
+
if "@FunctionCall" not in result:
|
|
41
|
+
file.write_text(result, encoding="utf-8")
|
|
42
|
+
self._delete_oldest_files(self.length) # type: ignore
|
|
43
|
+
|
|
44
|
+
return wrapper
|
|
45
|
+
|
|
46
|
+
@no_type_check
|
|
47
|
+
def _delete_oldest_files(self, max_files: int) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Class method to delete the oldest cached files in the CACHE_DIR folder.
|
|
50
|
+
|
|
51
|
+
:param max_files: Integer, the maximum number of files to keep in the CACHE_DIR folder.
|
|
52
|
+
"""
|
|
53
|
+
# Get all files in the folder.
|
|
54
|
+
files = self.cache_path.glob("*")
|
|
55
|
+
# Sort files by last modification time in ascending order.
|
|
56
|
+
files = sorted(files, key=lambda f: f.stat().st_mtime)
|
|
57
|
+
# Delete the oldest files if the number of files exceeds the limit.
|
|
58
|
+
if len(files) > max_files:
|
|
59
|
+
num_files_to_delete = len(files) - max_files
|
|
60
|
+
for i in range(num_files_to_delete):
|
|
61
|
+
files[i].unlink()
|
sgpt/config.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from getpass import getpass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from tempfile import gettempdir
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from click import UsageError
|
|
8
|
+
|
|
9
|
+
CONFIG_FOLDER = os.path.expanduser("~/.config")
|
|
10
|
+
SHELL_GPT_CONFIG_FOLDER = Path(CONFIG_FOLDER) / "shell_gpt"
|
|
11
|
+
SHELL_GPT_CONFIG_PATH = SHELL_GPT_CONFIG_FOLDER / ".sgptrc"
|
|
12
|
+
ROLE_STORAGE_PATH = SHELL_GPT_CONFIG_FOLDER / "roles"
|
|
13
|
+
FUNCTIONS_PATH = SHELL_GPT_CONFIG_FOLDER / "functions"
|
|
14
|
+
CHAT_CACHE_PATH = Path(gettempdir()) / "chat_cache"
|
|
15
|
+
CACHE_PATH = Path(gettempdir()) / "cache"
|
|
16
|
+
|
|
17
|
+
# TODO: Refactor ENV variables with SGPT_ prefix.
|
|
18
|
+
DEFAULT_CONFIG = {
|
|
19
|
+
# TODO: Refactor it to CHAT_STORAGE_PATH.
|
|
20
|
+
"CHAT_CACHE_PATH": os.getenv("CHAT_CACHE_PATH", str(CHAT_CACHE_PATH)),
|
|
21
|
+
"CACHE_PATH": os.getenv("CACHE_PATH", str(CACHE_PATH)),
|
|
22
|
+
"CHAT_CACHE_LENGTH": int(os.getenv("CHAT_CACHE_LENGTH", "100")),
|
|
23
|
+
"CACHE_LENGTH": int(os.getenv("CHAT_CACHE_LENGTH", "100")),
|
|
24
|
+
"REQUEST_TIMEOUT": int(os.getenv("REQUEST_TIMEOUT", "60")),
|
|
25
|
+
"DEFAULT_MODEL": os.getenv("DEFAULT_MODEL", "deepseek-v4-flash"),
|
|
26
|
+
"DEFAULT_COLOR": os.getenv("DEFAULT_COLOR", "magenta"),
|
|
27
|
+
"ROLE_STORAGE_PATH": os.getenv("ROLE_STORAGE_PATH", str(ROLE_STORAGE_PATH)),
|
|
28
|
+
"DEFAULT_EXECUTE_SHELL_CMD": os.getenv("DEFAULT_EXECUTE_SHELL_CMD", "false"),
|
|
29
|
+
"DISABLE_STREAMING": os.getenv("DISABLE_STREAMING", "false"),
|
|
30
|
+
"CODE_THEME": os.getenv("CODE_THEME", "dracula"),
|
|
31
|
+
"OPENAI_FUNCTIONS_PATH": os.getenv("OPENAI_FUNCTIONS_PATH", str(FUNCTIONS_PATH)),
|
|
32
|
+
"OPENAI_USE_FUNCTIONS": os.getenv("OPENAI_USE_FUNCTIONS", "true"),
|
|
33
|
+
"SHOW_FUNCTIONS_OUTPUT": os.getenv("SHOW_FUNCTIONS_OUTPUT", "false"),
|
|
34
|
+
"API_BASE_URL": os.getenv("API_BASE_URL", "https://api.deepseek.com"),
|
|
35
|
+
"PRETTIFY_MARKDOWN": os.getenv("PRETTIFY_MARKDOWN", "true"),
|
|
36
|
+
"USE_LITELLM": os.getenv("USE_LITELLM", "false"),
|
|
37
|
+
"SHELL_INTERACTION": os.getenv("SHELL_INTERACTION ", "true"),
|
|
38
|
+
"OS_NAME": os.getenv("OS_NAME", "auto"),
|
|
39
|
+
"SHELL_NAME": os.getenv("SHELL_NAME", "auto"),
|
|
40
|
+
# New features might add their own config variables here.
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Config(dict): # type: ignore
|
|
45
|
+
def __init__(self, config_path: Path, **defaults: Any):
|
|
46
|
+
self.config_path = config_path
|
|
47
|
+
|
|
48
|
+
if self._exists:
|
|
49
|
+
self._read()
|
|
50
|
+
has_new_config = False
|
|
51
|
+
for key, value in defaults.items():
|
|
52
|
+
if key not in self:
|
|
53
|
+
has_new_config = True
|
|
54
|
+
self[key] = value
|
|
55
|
+
if has_new_config:
|
|
56
|
+
self._write()
|
|
57
|
+
else:
|
|
58
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
# Don't write API key to config file if it is in the environment.
|
|
60
|
+
if not defaults.get("OPENAI_API_KEY") and not os.getenv("OPENAI_API_KEY"):
|
|
61
|
+
__api_key = getpass(prompt="Please enter your DeepSeek API key: ")
|
|
62
|
+
defaults["OPENAI_API_KEY"] = __api_key
|
|
63
|
+
super().__init__(**defaults)
|
|
64
|
+
self._write()
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def _exists(self) -> bool:
|
|
68
|
+
return self.config_path.exists()
|
|
69
|
+
|
|
70
|
+
def _write(self) -> None:
|
|
71
|
+
with open(self.config_path, "w", encoding="utf-8") as file:
|
|
72
|
+
string_config = ""
|
|
73
|
+
for key, value in self.items():
|
|
74
|
+
string_config += f"{key}={value}\n"
|
|
75
|
+
file.write(string_config)
|
|
76
|
+
|
|
77
|
+
def _read(self) -> None:
|
|
78
|
+
with open(self.config_path, "r", encoding="utf-8") as file:
|
|
79
|
+
for line in file:
|
|
80
|
+
if line.strip() and not line.startswith("#"):
|
|
81
|
+
key, value = line.strip().split("=", 1)
|
|
82
|
+
self[key] = value
|
|
83
|
+
|
|
84
|
+
def get(self, key: str) -> str: # type: ignore
|
|
85
|
+
# Prioritize environment variables over config file.
|
|
86
|
+
value = os.getenv(key) or super().get(key)
|
|
87
|
+
if not value:
|
|
88
|
+
raise UsageError(f"Missing config key: {key}")
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
cfg = Config(SHELL_GPT_CONFIG_PATH, **DEFAULT_CONFIG)
|
sgpt/function.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Callable, Dict, List
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from .config import cfg
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Function:
|
|
12
|
+
def __init__(self, path: str):
|
|
13
|
+
module = self._read(path)
|
|
14
|
+
self._function = module.Function.execute
|
|
15
|
+
self._openai_schema = module.Function.openai_schema()
|
|
16
|
+
self._name = self._openai_schema["function"]["name"]
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def name(self) -> str:
|
|
20
|
+
return self._name # type: ignore
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def openai_schema(self) -> dict[str, Any]:
|
|
24
|
+
return self._openai_schema # type: ignore
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def execute(self) -> Callable[..., str]:
|
|
28
|
+
return self._function # type: ignore
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def _read(cls, path: str) -> Any:
|
|
32
|
+
module_name = path.replace("/", ".").rstrip(".py")
|
|
33
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
34
|
+
module = importlib.util.module_from_spec(spec) # type: ignore
|
|
35
|
+
sys.modules[module_name] = module
|
|
36
|
+
spec.loader.exec_module(module) # type: ignore
|
|
37
|
+
|
|
38
|
+
if not issubclass(module.Function, BaseModel):
|
|
39
|
+
raise TypeError(
|
|
40
|
+
f"Function {module_name} must be a subclass of pydantic.BaseModel"
|
|
41
|
+
)
|
|
42
|
+
if not hasattr(module.Function, "execute"):
|
|
43
|
+
raise TypeError(
|
|
44
|
+
f"Function {module_name} must have an 'execute' classmethod"
|
|
45
|
+
)
|
|
46
|
+
if not hasattr(module.Function, "openai_schema"):
|
|
47
|
+
raise TypeError(
|
|
48
|
+
f"Function {module_name} must have an 'openai_schema' classmethod"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return module
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
functions_folder = Path(cfg.get("OPENAI_FUNCTIONS_PATH"))
|
|
55
|
+
functions_folder.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
functions = [Function(str(path)) for path in functions_folder.glob("*.py")]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_function(name: str) -> Callable[..., Any]:
|
|
60
|
+
for function in functions:
|
|
61
|
+
if function.name == name:
|
|
62
|
+
return function.execute
|
|
63
|
+
raise ValueError(f"Function {name} not found")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_openai_schemas() -> List[Dict[str, Any]]:
|
|
67
|
+
return [function.openai_schema for function in functions]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import locale
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, ClassVar, Dict, List # 新增 ClassVar, List 导入
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Function(BaseModel):
|
|
10
|
+
"""
|
|
11
|
+
执行一个 Shell 命令并返回输出(结果)。
|
|
12
|
+
注意:危险命令会被自动拦截,确保系统安全。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
shell_command: str = Field(
|
|
16
|
+
...,
|
|
17
|
+
example="dir",
|
|
18
|
+
description="要执行的 shell 命令。",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# 关键修改:添加 ClassVar[List[str]] 类型注解
|
|
22
|
+
DANGEROUS_PATTERNS: ClassVar[List[str]] = [
|
|
23
|
+
r"\bdel\b",
|
|
24
|
+
r"\berase\b",
|
|
25
|
+
r"\brmdir\b",
|
|
26
|
+
r"\brd\b",
|
|
27
|
+
r"\bformat\b",
|
|
28
|
+
r"\bshutdown\b",
|
|
29
|
+
r"\brestart\b",
|
|
30
|
+
r"\blogoff\b",
|
|
31
|
+
r"\breg\s+delete\b",
|
|
32
|
+
r"\breg\s+add\b.*\/f",
|
|
33
|
+
r"\bicacls\b",
|
|
34
|
+
r"\btakeown\b",
|
|
35
|
+
r"\bnet\s+user\b.*\/delete",
|
|
36
|
+
r"\bnet\s+localgroup\b.*\/delete",
|
|
37
|
+
r"\bsc\s+stop\b",
|
|
38
|
+
r"\bsc\s+delete\b",
|
|
39
|
+
r"\bwmic\s+process\s+call\s+create\b",
|
|
40
|
+
r"\bschtasks\b.*\/delete",
|
|
41
|
+
r"\bpowershell\s+.*-EncodedCommand\b",
|
|
42
|
+
r"\bcmd\s+/c\s+.*del\b",
|
|
43
|
+
r"&&\s*del\b",
|
|
44
|
+
r"\|\s*del\b",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def _is_dangerous(cls, command: str) -> bool:
|
|
49
|
+
"""检查命令是否命中危险模式"""
|
|
50
|
+
command_lower = command.lower()
|
|
51
|
+
for pattern in cls.DANGEROUS_PATTERNS:
|
|
52
|
+
if re.search(pattern, command_lower):
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def execute(cls, shell_command: str) -> str:
|
|
58
|
+
if cls._is_dangerous(shell_command):
|
|
59
|
+
return (
|
|
60
|
+
"Error: Potentially dangerous command detected and blocked.\n"
|
|
61
|
+
"If you believe this is a safe operation, please rephrase it "
|
|
62
|
+
"or execute it manually with proper authorization."
|
|
63
|
+
)
|
|
64
|
+
process = subprocess.Popen(
|
|
65
|
+
shell_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
|
66
|
+
)
|
|
67
|
+
output, _ = process.communicate()
|
|
68
|
+
exit_code = process.returncode
|
|
69
|
+
encoding = locale.getpreferredencoding()
|
|
70
|
+
return f"Exit code: {exit_code}, Output:\n{output.decode(encoding, errors='replace')}"
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def openai_schema(cls) -> Dict[str, Any]:
|
|
74
|
+
schema = cls.model_json_schema()
|
|
75
|
+
return {
|
|
76
|
+
"type": "function",
|
|
77
|
+
"function": {
|
|
78
|
+
"name": "execute_shell_command",
|
|
79
|
+
"description": cls.__doc__.strip() if cls.__doc__ else "",
|
|
80
|
+
"parameters": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"properties": schema.get("properties", {}),
|
|
83
|
+
"required": schema.get("required", []),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
|
|
6
|
+
class Function(BaseModel):
|
|
7
|
+
"""
|
|
8
|
+
抓取指定 URL 的网页文本内容(提取正文)。
|
|
9
|
+
当需要查看某个链接的具体内容时调用此函数。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
url: str = Field(..., description="要抓取的网页完整网址,例如 'https://example.com'。")
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def execute(cls, url: str) -> str:
|
|
16
|
+
try:
|
|
17
|
+
headers = {"User-Agent": "Mozilla/5.0"}
|
|
18
|
+
resp = requests.get(url, headers=headers, timeout=15)
|
|
19
|
+
resp.raise_for_status()
|
|
20
|
+
soup = BeautifulSoup(resp.text, "html.parser")
|
|
21
|
+
# 移除脚本和样式
|
|
22
|
+
for tag in soup(["script", "style", "nav", "footer", "header"]):
|
|
23
|
+
tag.decompose()
|
|
24
|
+
text = soup.get_text(separator="\n", strip=True)
|
|
25
|
+
# 限制长度,避免超出 token 限制
|
|
26
|
+
return text[:3000]
|
|
27
|
+
except Exception as e:
|
|
28
|
+
return f"抓取网页出错:{e}"
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def openai_schema(cls) -> Dict[str, Any]:
|
|
32
|
+
schema = cls.model_json_schema()
|
|
33
|
+
return {
|
|
34
|
+
"type": "function",
|
|
35
|
+
"function": {
|
|
36
|
+
"name": "fetch_url",
|
|
37
|
+
"description": cls.__doc__.strip() if cls.__doc__ else "",
|
|
38
|
+
"parameters": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": schema.get("properties", {}),
|
|
41
|
+
"required": schema.get("required", []),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
sgpt/functions/recall.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
MEMORY_FILE = Path.home() / ".shell_gpt_memory.txt"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Function(BaseModel):
|
|
11
|
+
query: str = Field(
|
|
12
|
+
...,
|
|
13
|
+
description="用于回忆记忆的关键词或问题(所有记忆都会被返回)",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def execute(cls, query: str) -> str:
|
|
18
|
+
if not MEMORY_FILE.exists():
|
|
19
|
+
return "当前没有任何保存的记忆。"
|
|
20
|
+
content = MEMORY_FILE.read_text(encoding="utf-8")
|
|
21
|
+
if not content.strip():
|
|
22
|
+
return "当前没有任何保存的记忆。"
|
|
23
|
+
return f"已保存的记忆:\n{content.strip()}"
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def openai_schema(cls) -> Dict[str, Any]:
|
|
27
|
+
schema = cls.model_json_schema()
|
|
28
|
+
return {
|
|
29
|
+
"type": "function",
|
|
30
|
+
"function": {
|
|
31
|
+
"name": "recall",
|
|
32
|
+
"description": cls.__doc__.strip() if cls.__doc__ else "",
|
|
33
|
+
"parameters": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": schema.get("properties", {}),
|
|
36
|
+
"required": schema.get("required", []),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|