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 ADDED
@@ -0,0 +1,5 @@
1
+ from .init_config import deploy_user_config
2
+ deploy_user_config()
3
+
4
+ from .app import main as main
5
+ from .app import entry_point as cli # noqa: F401
sgpt/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .app import entry_point
2
+
3
+ entry_point()
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
+ }
@@ -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
+ }