返回提交历史
Modified
g4f/__main__.py
+1
-1
Renamed
g4f/cli/__main__.py
+4
-0
Added
g4f/cli/client.py
+222
-0
Modified
setup.py
+45
-24
XFEstudio/gpt4free
Add g4f cli client
eff8624e
代码差异
4 个文件
+272
-25
@@ -1,6 +1,6 @@
1
1
from __future__ import annotations
2
2
3
from .cli import get_api_parser, run_api_args
3
from .cli.__main__ import get_api_parser, run_api_args
4
4
5
5
parser = get_api_parser()
6
6
args = parser.parse_args()
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
3
3
import argparse
4
4
from argparse import ArgumentParser
5
from .client import get_parser, run_client_args
5
6
6
7
from g4f import Provider
7
8
from g4f.gui.run import gui_parser, run_gui_args
@@ -41,12 +42,15 @@ def main():
41
42
subparsers = parser.add_subparsers(dest="mode", help="Mode to run the g4f in.")
42
43
subparsers.add_parser("api", parents=[get_api_parser()], add_help=False)
43
44
subparsers.add_parser("gui", parents=[gui_parser()], add_help=False)
45
subparsers.add_parser("client", parents=[get_parser()], add_help=False)
44
46
45
47
args = parser.parse_args()
46
48
if args.mode == "api":
47
49
run_api_args(args)
48
50
elif args.mode == "gui":
49
51
run_gui_args(args)
52
elif args.mode == "client":
53
run_client_args(args)
50
54
else:
51
55
parser.print_help()
52
56
exit(1)
@@ -0,0 +1,222 @@
1
#!/usr/bin/env python3
2
3
import os
4
import sys
5
import asyncio
6
import json
7
import argparse
8
from pathlib import Path
9
from typing import Optional, List, Dict
10
from g4f.client import AsyncClient
11
from g4f.providers.response import JsonConversation
12
from g4f.cookies import set_cookies_dir, read_cookie_files
13
from g4f.Provider import ProviderUtils
14
from g4f import debug
15
16
# Platform-appropriate directories
17
def get_config_dir() -> Path:
18
"""Get platform-appropriate config directory."""
19
if sys.platform == "win32":
20
return Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
21
elif sys.platform == "darwin":
22
return Path.home() / "Library" / "Application Support"
23
else: # Linux and other UNIX-like
24
return Path.home() / ".config"
25
26
CONFIG_DIR = get_config_dir() / "g4f-cli"
27
COOKIES_DIR = CONFIG_DIR / "cookies"
28
CONVERSATION_FILE = CONFIG_DIR / "conversation.json"
29
30
class ConversationManager:
31
"""Manages conversation history and state."""
32
33
def __init__(self, file_path: Path, model: Optional[str] = None, provider: Optional[str] = None) -> None:
34
self.file_path = file_path
35
self.model: Optional[str] = model
36
self.provider: Optional[str] = provider
37
self.conversation = None
38
self.history: List[Dict[str, str]] = []
39
self._load()
40
41
def _load(self) -> None:
42
"""Load conversation from file."""
43
if not self.file_path.exists():
44
return
45
46
try:
47
with open(self.file_path, 'r', encoding='utf-8') as f:
48
data = json.load(f)
49
self.model = data.get("model")
50
self.provider = data.get("provider") if self.provider is None else self.provider
51
self.data = data.get("data", {})
52
if self.provider and data.get(self.provider):
53
self.conversation = JsonConversation(**data.get(self.provider))
54
self.history = data.get("history", [])
55
except (json.JSONDecodeError, KeyError) as e:
56
print(f"Error loading conversation: {e}", file=sys.stderr)
57
except Exception as e:
58
print(f"Unexpected error loading conversation: {e}", file=sys.stderr)
59
60
def save(self) -> None:
61
"""Save conversation to file."""
62
try:
63
with open(self.file_path, 'w', encoding='utf-8') as f:
64
if self.conversation and self.provider:
65
self.data[self.provider] = self.conversation.get_dict()
66
else:
67
self.data = self.conversation.get_dict() if self.conversation else {}
68
json.dump({
69
"model": self.model,
70
"provider": self.provider,
71
"data": self.data,
72
"history": self.history
73
}, f, indent=2, ensure_ascii=False)
74
except Exception as e:
75
print(f"Error saving conversation: {e}", file=sys.stderr)
76
77
def add_message(self, role: str, content: str) -> None:
78
"""Add a message to the conversation."""
79
self.history.append({"role": role, "content": content})
80
81
def get_messages(self) -> List[Dict[str, str]]:
82
"""Get all messages in the conversation."""
83
return self.history
84
85
async def stream_response(
86
client: AsyncClient,
87
input_text: str,
88
conversation: ConversationManager
89
) -> None:
90
"""Stream the response from the API and update conversation."""
91
input_text = input_text.strip()
92
if not input_text:
93
raise ValueError("Input text cannot be empty")
94
95
# Add user message to conversation
96
conversation.add_message("user", input_text)
97
98
create_args = {
99
"messages": conversation.get_messages(),
100
"stream": True
101
}
102
103
if conversation.model:
104
create_args["model"] = conversation.model
105
106
response_content = []
107
last_chunk = None
108
async for chunk in client.chat.completions.create(**create_args):
109
last_chunk = chunk
110
token = chunk.choices[0].delta.content
111
if not token:
112
continue
113
token = str(token)
114
response_content.append(token)
115
try:
116
for byte in token.encode('utf-8'):
117
sys.stdout.buffer.write(bytes([byte]))
118
sys.stdout.buffer.flush()
119
except (IOError, BrokenPipeError) as e:
120
print(f"\nError writing to stdout: {e}", file=sys.stderr)
121
break
122
123
conversation.conversation = getattr(last_chunk, 'conversation', None)
124
125
if response_content:
126
# Add assistant message to conversation
127
full_response = "".join(response_content).strip()
128
conversation.add_message("assistant", full_response)
129
else:
130
raise RuntimeError("No response received from the API")
131
132
def get_parser():
133
"""Parse command line arguments."""
134
parser = argparse.ArgumentParser(
135
description="GPT CLI client with conversation history",
136
formatter_class=argparse.ArgumentDefaultsHelpFormatter
137
)
138
parser.add_argument("--debug", "-d", action="store_true", help="Enable verbose logging.")
139
parser.add_argument(
140
'-p', '--provider',
141
default=None,
142
help=f"Provider to use. Available: {', '.join([key for key, provider in ProviderUtils.convert.items() if provider.working])}."
143
)
144
parser.add_argument(
145
'-m', '--model',
146
help="Model to use (provider-specific)"
147
)
148
parser.add_argument(
149
'-c', '--cookies-dir',
150
type=Path,
151
default=COOKIES_DIR,
152
help="Directory containing cookies for authenticated providers"
153
)
154
parser.add_argument(
155
'--conversation-file',
156
type=Path,
157
default=CONVERSATION_FILE,
158
help="File to store/load conversation state"
159
)
160
parser.add_argument(
161
'--clear-history',
162
action='store_true',
163
help="Clear conversation history before starting"
164
)
165
parser.add_argument(
166
'input',
167
nargs='?',
168
help="Input text (or read from stdin)"
169
)
170
171
return parser
172
173
async def run_args(input_text: str, args):
174
try:
175
# Ensure directories exist
176
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
177
COOKIES_DIR.mkdir(parents=True, exist_ok=True)
178
179
if args.debug:
180
debug.logging = True
181
182
# Initialize conversation manager
183
conversation = ConversationManager(args.conversation_file, args.model, args.provider)
184
if args.clear_history:
185
conversation.history = []
186
conversation.conversation = None
187
188
# Set cookies directory if specified
189
set_cookies_dir(str(args.cookies_dir))
190
read_cookie_files()
191
192
# Initialize client with selected provider
193
client = AsyncClient(provider=conversation.provider)
194
195
# Stream response and update conversation
196
await stream_response(client, input_text, conversation)
197
198
# Save conversation state
199
conversation.save()
200
201
print() # Ensure final newline
202
203
except Exception as e:
204
print(f"\nError: {e}", file=sys.stderr)
205
sys.exit(1)
206
207
def run_client_args(args):
208
input_text = args.input
209
if not input_text:
210
input_text = sys.stdin.read().strip()
211
if not input_text:
212
print("No input provided. Use -h for help.", file=sys.stderr)
213
sys.exit(1)
214
if os.path.isfile(input_text):
215
with open(input_text, 'r', encoding='utf-8') as f:
216
input_text = f.read().strip()
217
# Run the client with provided arguments
218
asyncio.run(run_args(input_text, args))
219
220
if __name__ == "__main__":
221
# Run the client with command line arguments
222
run_client_args(get_parser().parse_args())
@@ -104,7 +104,7 @@ setup(
104
104
long_description=long_description,
105
105
packages=find_packages(),
106
106
package_data={
107
'g4f': ['g4f/inference/*', 'g4f/gui/client/*', 'g4f/gui/server/*', 'g4f/Provider/npm/*', 'g4f/local/models/*']
107
'g4f': []
108
108
},
109
109
include_package_data=True,
110
110
install_requires=INSTALL_REQUIRE,
@@ -118,29 +118,50 @@ setup(
118
118
'Bug Tracker': 'https://github.com/xtekky/gpt4free/issues', # Link to issue tracker
119
119
},
120
120
keywords=[
121
'python',
122
'chatbot',
123
'reverse-engineering',
124
'openai',
125
'chatbots',
126
'gpt',
127
'language-model',
128
'gpt-3',
129
'gpt3',
130
'openai-api',
131
'gpt-4',
132
'gpt4',
133
'chatgpt',
134
'chatgpt-api',
135
'openai-chatgpt',
136
'chatgpt-free',
137
'chatgpt-4',
138
'chatgpt4',
139
'chatgpt4-api',
140
'free',
141
'free-gpt',
142
'gpt4free',
143
'g4f',
121
"gpt4free",
122
"gpt4free.js",
123
"g4f",
124
"g4f.dev",
125
"javascript",
126
"npm",
127
"browser",
128
"gpt",
129
"chatgpt",
130
"deepseek",
131
"openai",
132
"ai",
133
"client",
134
"sdk",
135
"free",
136
"ai",
137
"gpt-4",
138
"gpt-4o",
139
"chat",
140
"api",
141
"browser",
142
"ai",
143
"ai",
144
"js",
145
"client",
146
"text",
147
"generation",
148
"image",
149
"generation",
150
"in-browser",
151
"ai",
152
"frontend",
153
"ai",
154
"openai",
155
"alternative",
156
"javascript",
157
"ai",
158
"library",
159
"nodejs",
160
"prompt",
161
"engineering",
162
"chatbot",
163
"ai",
164
"integration"
144
165
],
145
166
classifiers=[
146
167
'Development Status :: 2 - Pre-Alpha',