XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

Add -e option to CLI client

adb756ef
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

1 个文件 +189 -191
Modified g4f/cli/client.py +189 -191
@@ -7,6 +7,7 @@ import json
7 7 import argparse
8 8 import traceback
9 9 import requests
10
10 11 from pathlib import Path
11 12 from typing import Optional, List, Dict
12 13 from g4f.client import AsyncClient
@@ -17,96 +18,99 @@ from g4f.image import extract_data_uri, is_accepted_format
17 18 from g4f.image.copy_images import get_media_dir
18 19 from g4f.client.helper import filter_markdown
19 20 from g4f.errors import MissingRequirementsError
21
20 22 try:
21 23 from g4f.integration.markitdown import MarkItDown
22 24 has_markitdown = True
23 25 except ImportError:
24 26 has_markitdown = False
27
25 28 from g4f.config import CONFIG_DIR, COOKIES_DIR
26 29 from g4f import debug
27 30
28 31 CONVERSATION_FILE = CONFIG_DIR / "conversation.json"
29 32
33
30 34 class ConversationManager:
31 35 """Manages conversation history and state."""
32
33 def __init__(self, file_path: Optional[Path] = None, model: Optional[str] = None, provider: Optional[str] = None) -> None:
34 self.file_path: Optional[Path] = file_path
35 self.model: Optional[str] = model
36 self.provider: Optional[str] = provider
37 self.conversation = None
36 def __init__(
37 self,
38 file_path: Optional[Path] = None,
39 model: Optional[str] = None,
40 provider: Optional[str] = None,
41 max_messages: int = 5
42 ) -> None:
43 self.file_path = file_path
44 self.model = model
45 self.provider = provider
46 self.max_messages = max_messages
47 self.conversation: Optional[JsonConversation] = None
38 48 self.history: List[Dict[str, str]] = []
39 49 self.data: Dict = {}
40 50 self._load()
41 51
42 52 def _load(self) -> None:
43 """Load conversation from file."""
44 if self.file_path is None or not self.file_path.is_file():
53 if not self.file_path or not self.file_path.is_file():
45 54 return
46
47 55 try:
48 56 with open(self.file_path, 'r', encoding='utf-8') as f:
49 57 data = json.load(f)
50 self.model = data.get("model") if self.model is None and self.provider is None else self.model
51 self.provider = data.get("provider") if self.provider is None else self.provider
52 if not self.provider:
53 self.provider = None
54 self.data = data.get("data", {})
55 if self.provider and self.data.get(self.provider):
56 self.conversation = JsonConversation(**self.data.get(self.provider))
57 elif not self.provider and self.data:
58 self.conversation = JsonConversation(**self.data)
59 self.history = data.get("items", [])
60 except (json.JSONDecodeError, KeyError) as e:
61 print(f"Error loading conversation: {e}", file=sys.stderr)
58 if self.model is None:
59 self.model = data.get("model")
60 if self.provider is None:
61 self.provider = data.get("provider")
62 self.data = data.get("data", {})
63 if self.provider and self.data.get(self.provider):
64 self.conversation = JsonConversation(**self.data[self.provider])
65 elif not self.provider and self.data:
66 self.conversation = JsonConversation(**self.data)
67 self.history = data.get("items", [])
62 68 except Exception as e:
63 print(f"Unexpected error loading conversation: {e}", file=sys.stderr)
69 print(f"Error loading conversation: {e}", file=sys.stderr)
64 70
65 71 def save(self) -> None:
66 """Save conversation to file."""
67 if self.file_path is None:
72 if not self.file_path:
68 73 return
69
70 74 try:
75 if self.conversation and self.provider:
76 self.data[self.provider] = self.conversation.get_dict()
77 elif self.conversation:
78 self.data.update(self.conversation.get_dict())
79 payload = {
80 "model": self.model,
81 "provider": self.provider,
82 "data": self.data,
83 "items": self.history
84 }
71 85 with open(self.file_path, 'w', encoding='utf-8') as f:
72 if self.conversation and self.provider:
73 self.data[self.provider] = self.conversation.get_dict()
74 else:
75 self.data = {**self.data, **(self.conversation.get_dict() if self.conversation else {})}
76 json.dump({
77 "model": self.model,
78 "provider": self.provider,
79 "data": self.data,
80 "items": self.history
81 }, f, indent=2, ensure_ascii=False)
86 json.dump(payload, f, indent=2, ensure_ascii=False)
82 87 except Exception as e:
83 88 print(f"Error saving conversation: {e}", file=sys.stderr)
84 89
85 90 def add_message(self, role: str, content: str) -> None:
86 """Add a message to the conversation."""
87 91 self.history.append({"role": role, "content": content})
88 92
89 93 def get_messages(self) -> List[Dict[str, str]]:
90 """Get all messages in the conversation."""
91 return self.history
94 result = []
95 for item in self.history[-self.max_messages:]:
96 if item.get("role") in ["user", "system"] or result:
97 result.append(item)
98 return result
92 99
93 100 async def stream_response(
94 101 client: AsyncClient,
95 input_text: str,
102 input_text,
96 103 conversation: ConversationManager,
97 104 output_file: Optional[Path] = None,
98 105 instructions: Optional[str] = None
99 106 ) -> None:
100 """Stream the response from the API and update conversation."""
101 107 media = None
102 108 if isinstance(input_text, tuple):
103 109 media, input_text = input_text
104
110
105 111 if instructions:
106 # Add system instructions to conversation if provided
107 112 conversation.add_message("system", instructions)
108 113
109 # Add user message to conversation
110 114 conversation.add_message("user", input_text)
111 115
112 116 create_args = {
@@ -117,214 +121,208 @@ async def stream_response(
117 121 "conversation": conversation.conversation,
118 122 }
119 123
120 response_content = []
124 response_tokens = []
121 125 last_chunk = None
122 126 async for chunk in client.chat.completions.create(**create_args):
123 127 last_chunk = chunk
124 token = chunk.choices[0].delta.content
125 if not token:
128 delta = chunk.choices[0].delta.content
129 if not delta:
126 130 continue
127 if is_content(token):
128 response_content.append(token)
129 try:
130 print(token, end="", flush=True)
131 except (IOError, BrokenPipeError) as e:
132 print(f"\nError writing to stdout: {e}", file=sys.stderr)
133 break
134 print("\n", end="")
131 if is_content(delta):
132 response_tokens.append(delta)
133 print(delta, end="", flush=True)
134 print()
135
136 if last_chunk and hasattr(last_chunk, "conversation"):
137 conversation.conversation = last_chunk.conversation
138
139 media_chunk = next((t for t in response_tokens if isinstance(t, MediaResponse)), None)
140 text_response = ""
141 if media_chunk:
142 text_response = response_tokens[0] if len(response_tokens) == 1 else "".join(str(t) for t in response_tokens)
143 else:
144 text_response = "".join(str(t) for t in response_tokens)
135 145
136 conversation.conversation = getattr(last_chunk, "conversation", conversation.conversation)
137 media_content = next(iter([chunk for chunk in response_content if isinstance(chunk, MediaResponse)]), None)
138 response_content = response_content[0] if len(response_content) == 1 else "".join([str(chunk) for chunk in response_content])
139 146 if output_file:
140 if save_content(response_content, media_content, output_file):
141 print(f"\nResponse saved to {output_file}")
147 if save_content(text_response, media_chunk, str(output_file)):
148 print(f"\n→ Response saved to '{output_file}'")
142 149
143 if response_content:
144 # Add assistant message to conversation
145 conversation.add_message("assistant", str(response_content))
150 if text_response:
151 conversation.add_message("assistant", text_response)
146 152 else:
147 raise RuntimeError("No response received from the API")
153 raise RuntimeError("No response received")
148 154
149 def save_content(content, media_content: Optional[MediaResponse], filepath: str, allowed_types = None):
150 if media_content is not None:
151 for url in media_content.urls:
152 if url.startswith("http://") or url.startswith("https://"):
155
156 def save_content(content, media: Optional[MediaResponse], filepath: str, allowed_types=None) -> bool:
157 if media:
158 for url in media.urls:
159 if url.startswith(("http://", "https://")):
153 160 try:
154 response = requests.get(url, cookies=media_content.get("cookies"), headers=media_content.get("headers"))
155 if response.status_code == 200:
161 resp = requests.get(url, cookies=media.get("cookies"), headers=media.get("headers"))
162 if resp.status_code == 200:
156 163 with open(filepath, "wb") as f:
157 f.write(response.content)
164 f.write(resp.content)
158 165 return True
159 except requests.RequestException as e:
160 print(f"Error downloading {url}: {e}", file=sys.stderr)
166 except Exception as e:
167 print(f"Error fetching media '{url}': {e}", file=sys.stderr)
161 168 return False
162 169 else:
163 170 content = url
164 171 break
165 elif hasattr(content, "data"):
172 if hasattr(content, "data"):
166 173 content = content.data
167 174 if not content:
168 175 print("\nNo content to save.", file=sys.stderr)
169 176 return False
170 if content.startswith("/media/"):
171 os.rename(content.replace("/media", get_media_dir()).split("?")[0], filepath)
172 return True
173 elif content.startswith("data:"):
177 if content.startswith("data:"):
174 178 with open(filepath, "wb") as f:
175 179 f.write(extract_data_uri(content))
176 180 return True
177 content = filter_markdown(content, allowed_types)
178 if content:
179 with open(filepath, "w") as f:
180 f.write(content)
181 return True
182 else:
183 print("\nNo valid content to save.", file=sys.stderr)
184 return False
181 if content.startswith("/media/"):
182 src = content.replace("/media", get_media_dir()).split("?")[0]
183 os.rename(src, filepath)
184 return True
185 filtered = filter_markdown(content, allowed_types)
186 if filtered:
187 with open(filepath, "w", encoding="utf-8") as f:
188 f.write(filtered)
189 return True
190 print("\nUnable to save content.", file=sys.stderr)
191 return False
185 192
186 193 def get_parser():
187 """Parse command line arguments."""
188 194 parser = argparse.ArgumentParser(
189 195 description="G4F CLI client with conversation history",
190 196 formatter_class=argparse.ArgumentDefaultsHelpFormatter
191 197 )
192 parser.add_argument("--debug", "-d", action="store_true", help="Enable verbose logging.")
198 parser.add_argument('-d', '--debug', action='store_true', help="Verbose debug")
199 parser.add_argument('-p', '--provider', default=None,
200 help=f"Provider to use: {', '.join(k for k,v in ProviderUtils.convert.items() if v.working)}")
201 parser.add_argument('-m', '--model', help="Model name")
202 parser.add_argument('-O', '--output', type=Path,
203 help="Save assistant output to FILE (text or media)")
204 parser.add_argument('-i', '--instructions', help="System instructions")
205 parser.add_argument('-c', '--cookies-dir', type=Path, default=COOKIES_DIR,
206 help="Cookies/HAR directory")
207 parser.add_argument('--conversation-file', type=Path, default=CONVERSATION_FILE,
208 help="Conversation JSON")
209 parser.add_argument('-C', '--clear-history', action='store_true', help="Wipe history")
210 parser.add_argument('-N', '--no-config', action='store_true', help="Skip loading history")
211 # <-- updated -e/--edit to take an optional filename
193 212 parser.add_argument(
194 '-p', '--provider',
195 default=None,
196 help=f"Provider to use. Available: {', '.join([key for key, provider in ProviderUtils.convert.items() if provider.working])}."
197 )
198 parser.add_argument(
199 '-m', '--model',
200 help="Model to use (provider-specific)"
201 )
202 parser.add_argument(
203 '-O', '--output',
204 default=None,
213 '-e', '--edit',
205 214 type=Path,
206 215 metavar='FILE',
207 help="Output file to save the response file."
216 help="If FILE given: send its contents and overwrite it with AI's reply."
208 217 )
209 parser.add_argument(
210 '-i', '--instructions',
211 default=None,
212 help="Add custom system instructions."
213 )
214 parser.add_argument(
215 '-c', '--cookies-dir',
216 type=Path,
217 default=COOKIES_DIR,
218 help="Directory containing cookies for authenticated providers"
219 )
220 parser.add_argument(
221 '--conversation-file',
222 type=Path,
223 metavar='FILE',
224 default=CONVERSATION_FILE,
225 help="File to store/load conversation state"
226 )
227 parser.add_argument(
228 '-C', '--clear-history',
229 action='store_true',
230 help="Clear conversation history before starting"
231 )
232 parser.add_argument(
233 '-N', '--no-config',
234 action='store_true',
235 help="Do not load configuration from conversation file"
236 )
237 parser.add_argument(
238 'input',
239 nargs='*',
240 help="Input urls, files and text (or read from stdin)"
241 )
242
218 parser.add_argument('--max-messages', type=int, default=5,
219 help="Max user+assistant turns in context")
220 parser.add_argument('input', nargs='*',
221 help="URLs, image paths or plain text")
243 222 return parser
244 223
245 async def run_args(input_text: str, args):
224
225 async def run_args(input_val, args):
246 226 try:
247 # Ensure directories exist
227 # ensure dirs
248 228 if args.output:
249 229 args.output.parent.mkdir(parents=True, exist_ok=True)
250 args.conversation_file.parent.mkdir(parents=True, exist_ok=True)
230 if args.conversation_file:
231 args.conversation_file.parent.mkdir(parents=True, exist_ok=True)
251 232 args.cookies_dir.mkdir(parents=True, exist_ok=True)
252 233
253 234 if args.debug:
254 235 debug.logging = True
255
256 # Initialize conversation manager
257 conversation = ConversationManager(None if args.no_config else args.conversation_file, args.model, args.provider)
236
237 conv = ConversationManager(
238 None if args.no_config else args.conversation_file,
239 model=args.model,
240 provider=args.provider,
241 max_messages=args.max_messages
242 )
258 243 if args.clear_history:
259 conversation.history = []
260 conversation.conversation = None
244 conv.history = []
245 conv.conversation = None
261 246
262 # Set cookies directory if specified
263 247 set_cookies_dir(str(args.cookies_dir))
264 248 read_cookie_files()
265
266 # Initialize client with selected provider
267 client = AsyncClient(provider=conversation.provider)
268
269 # Stream response and update conversation
270 await stream_response(client, input_text, conversation, args.output, args.instructions)
271
272 # Save conversation state
273 conversation.save()
274 except:
249
250 client = AsyncClient(provider=conv.provider)
251
252 if isinstance(args.edit, Path):
253 file_to_edit = args.edit
254 if not file_to_edit.exists():
255 print(f"ERROR: file not found: {file_to_edit}", file=sys.stderr)
256 sys.exit(1)
257 text = file_to_edit.read_text(encoding="utf-8")
258 # we will both send and overwrite this file
259 input_val = f"```file: {file_to_edit}\n{text}\n```\n" + (input_val[1] if isinstance(input_val, tuple) else input_val)
260 output_target = file_to_edit
261 else:
262 # normal, non-edit mode
263 output_target = args.output
264
265 await stream_response(client, input_val, conv, output_target, args.instructions)
266 conv.save()
267
268 except Exception:
275 269 print(traceback.format_exc(), file=sys.stderr)
276 270 sys.exit(1)
277 271
272
278 273 def run_client_args(args):
279 input_text = ""
274 input_txt = ""
280 275 media = []
281 276 rest = 0
282 for idx, input_value in enumerate(args.input):
283 if input_value.startswith("http://") or input_value.startswith("https://"):
284 response = requests.head(input_value)
285 if not response.ok:
286 print(f"Error accessing URL {input_value}: {response.status_code}", file=sys.stderr)
287 break
288 if response.headers.get('Content-Type', '').startswith('image/'):
289 media.append(input_value)
277
278 for idx, tok in enumerate(args.input):
279 if tok.startswith(("http://","https://")):
280 # same URL logic...
281 resp = requests.head(tok, allow_redirects=True)
282 if resp.ok and resp.headers.get("Content-Type","").startswith("image"):
283 media.append(tok)
290 284 else:
291 try:
292 if not has_markitdown:
293 raise MissingRequirementsError("MarkItDown is not installed. Install it with `pip install -U markitdown`.")
294 md = MarkItDown()
295 text_content = md.convert_url(input_value).text_content
296 input_text += f"\n```\n{text_content}\n\nSource: {input_value}\n```\n"
297 except Exception as e:
298 print(f"Error processing URL {input_value}: {type(e).__name__}: {e}", file=sys.stderr)
299 break
300 elif os.path.isfile(input_value):
285 if not has_markitdown:
286 raise MissingRequirementsError("Install markitdown")
287 md = MarkItDown()
288 txt = md.convert_url(tok).text_content
289 input_txt += f"\n```source: {tok}\n{txt}\n```\n"
290 elif os.path.isfile(tok):
291 head = Path(tok).read_bytes()[:12]
301 292 try:
302 with open(input_value, 'rb') as f:
303 if is_accepted_format(f.read(12)):
304 media.append(Path(input_value))
293 if is_accepted_format(head):
294 media.append(Path(tok))
295 is_img = True
296 else:
297 is_img = False
305 298 except ValueError:
306 # If not a valid image, read as text
307 try:
308 with open(input_value, 'r', encoding='utf-8') as f:
309 file_content = f.read().strip()
310 except UnicodeDecodeError:
311 print(f"Error reading file {input_value} as text. Ensure it is a valid text file.", file=sys.stderr)
312 break
313 input_text += f"\n```{input_value}\n{file_content}\n```\n"
299 is_img = False
300 if not is_img:
301 txt = Path(tok).read_text(encoding="utf-8")
302 input_txt += f"\n```file: {tok}\n{txt}\n```\n"
314 303 else:
304 rest = idx
315 305 break
316 306 rest = idx + 1
317 input_text = (" ".join(args.input[rest:])).strip() + input_text
307
308 tail = args.input[rest:]
309 if tail:
310 input_txt = " ".join(tail) + "\n" + input_txt
311
312 if not sys.stdin.isatty() and not input_txt:
313 input_txt = sys.stdin.read()
314
318 315 if media:
319 input_text = (media, input_text)
320 if not sys.stdin.isatty() and not input_text:
321 input_text = sys.stdin.read().strip()
322 if not input_text:
323 print("No input provided. Use -h for help.", file=sys.stderr)
316 val = (media, input_txt)
317 else:
318 val = input_txt.strip()
319
320 if not val:
321 print("No input provided. Use -h.", file=sys.stderr)
324 322 sys.exit(1)
325 # Run the client with provided arguments
326 asyncio.run(run_args(input_text, args))
323
324 asyncio.run(run_args(val, args))
325
327 326
328 327 if __name__ == "__main__":
329 # Run the client with command line arguments
330 328 run_client_args(get_parser().parse_args())