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

XFEstudio/gpt4free

Fix missing websocket_request_id in OpenaiChat Add RateLimitErrors to Bing Add android support to gui api Add annotations import to gui api

486e43da
Heiner Lohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

12 个文件 +243 -53
Modified .gitignore +2 -1
@@ -52,4 +52,5 @@ x.py
52 52 info.txt
53 53 local.py
54 54 *.gguf
55 image.py
55 image.py
56 .buildozer
Modified g4f/Provider/Bing.py +15 -11
@@ -11,7 +11,7 @@ from aiohttp import ClientSession, ClientTimeout, BaseConnector, WSMsgType
11 11
12 12 from ..typing import AsyncResult, Messages, ImageType, Cookies
13 13 from ..image import ImageRequest
14 from ..errors import ResponseStatusError
14 from ..errors import ResponseStatusError, RateLimitError
15 15 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
16 16 from .helper import get_connector, get_random_hex
17 17 from .bing.upload_image import upload_image
@@ -26,7 +26,7 @@ class Tones:
26 26 creative = "Creative"
27 27 balanced = "Balanced"
28 28 precise = "Precise"
29 copilot = "Balanced"
29 copilot = "Copilot"
30 30
31 31 class Bing(AsyncGeneratorProvider, ProviderModelMixin):
32 32 """
@@ -36,8 +36,8 @@ class Bing(AsyncGeneratorProvider, ProviderModelMixin):
36 36 working = True
37 37 supports_message_history = True
38 38 supports_gpt_4 = True
39 default_model = "balanced"
40 models = [key for key in Tones.__dict__ if not key.startswith("__")]
39 default_model = "Balanced"
40 models = [getattr(Tones, key) for key in Tones.__dict__ if not key.startswith("__")]
41 41
42 42 @classmethod
43 43 def create_async_generator(
@@ -72,7 +72,7 @@ class Bing(AsyncGeneratorProvider, ProviderModelMixin):
72 72 context = create_context(messages[:-1]) if len(messages) > 1 else None
73 73 if tone is None:
74 74 tone = tone if model.startswith("gpt-4") else model
75 tone = cls.get_model("" if tone is None else tone.lower())
75 tone = cls.get_model("" if tone is None else tone)
76 76 gpt4_turbo = True if model.startswith("gpt-4-turbo") else False
77 77
78 78 return stream_generate(
@@ -258,7 +258,6 @@ class Defaults:
258 258 'sec-fetch-mode': 'cors',
259 259 'sec-fetch-dest': 'empty',
260 260 'referer': home,
261 'accept-encoding': 'gzip, deflate, br',
262 261 'accept-language': 'en-US,en;q=0.9',
263 262 }
264 263
@@ -311,7 +310,7 @@ def create_message(
311 310 "allowedMessageTypes": Defaults.allowedMessageTypes,
312 311 "sliceIds": Defaults.sliceIds[tone],
313 312 "verbosity": "verbose",
314 "scenario": "CopilotMicrosoftCom" if tone == "copilot" else "SERP",
313 "scenario": "CopilotMicrosoftCom" if tone == Tones.copilot else "SERP",
315 314 "plugins": [{"id": "c310c353-b9f0-4d76-ab0d-1dd5e979cf68", "category": 1}] if web_search else [],
316 315 "traceId": get_random_hex(40),
317 316 "conversationHistoryOptionsSets": ["autosave","savemem","uprofupd","uprofgen"],
@@ -329,7 +328,7 @@ def create_message(
329 328 "requestId": request_id,
330 329 "messageId": request_id
331 330 },
332 "tone": getattr(Tones, tone),
331 "tone": "Balanced" if tone == Tones.copilot else tone,
333 332 "spokenTextMode": "None",
334 333 "conversationId": conversation.conversationId,
335 334 "participant": {"id": conversation.clientId}
@@ -412,10 +411,15 @@ async def stream_generate(
412 411 await asyncio.sleep(sleep_retry)
413 412 continue
414 413
415 image_request = await upload_image(session, image, getattr(Tones, tone), headers) if image else None
414 image_request = await upload_image(
415 session,
416 image,
417 "Balanced" if Tones.copilot == "Copilot" else tone,
418 headers
419 ) if image else None
416 420 async with session.ws_connect(
417 421 'wss://s.copilot.microsoft.com/sydney/ChatHub'
418 if tone == "copilot" else
422 if tone == "Copilot" else
419 423 'wss://sydney.bing.com/sydney/ChatHub',
420 424 autoping=False,
421 425 params={'sec_access_token': conversation.conversationSignature},
@@ -481,7 +485,7 @@ async def stream_generate(
481 485 max_retries -= 1
482 486 if max_retries < 1:
483 487 if result["value"] == "CaptchaChallenge":
484 raise RuntimeError(f"{result['value']}: Use other cookies or/and ip address")
488 raise RateLimitError(f"{result['value']}: Use other cookies or/and ip address")
485 489 else:
486 490 raise RuntimeError(f"{result['value']}: {result['message']}")
487 491 if debug.logging:
Modified g4f/Provider/bing/conversation.py +3 -0
@@ -2,6 +2,7 @@ from __future__ import annotations
2 2
3 3 from aiohttp import ClientSession
4 4 from ...requests import raise_for_status
5 from ...errors import RateLimitError
5 6
6 7 class Conversation:
7 8 """
@@ -36,6 +37,8 @@ async def create_conversation(session: ClientSession, headers: dict, tone: str)
36 37 else:
37 38 url = "https://www.bing.com/turing/conversation/create?bundleVersion=1.1626.1"
38 39 async with session.get(url, headers=headers) as response:
40 if response.status == 404:
41 raise RateLimitError("Response 404: Do less requests and reuse conversations")
39 42 await raise_for_status(response, "Failed to create conversation")
40 43 data = await response.json()
41 44 conversationId = data.get('conversationId')
Modified g4f/Provider/needs_auth/OpenaiChat.py +1 -1
@@ -450,7 +450,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
450 450 ) as response:
451 451 cls._update_request_args(session)
452 452 await raise_for_status(response)
453 async for chunk in cls.iter_messages_chunk(response.iter_lines(), session, fields, websocket_request_id):
453 async for chunk in cls.iter_messages_chunk(response.iter_lines(), session, fields):
454 454 if response_fields:
455 455 response_fields = False
456 456 yield fields
Modified g4f/gui/client/index.html +3 -3
@@ -133,15 +133,15 @@
133 133 <div class="box input-box">
134 134 <textarea id="message-input" placeholder="Ask a question" cols="30" rows="10"
135 135 style="white-space: pre-wrap;resize: none;"></textarea>
136 <label for="image" title="Works with Bing, Gemini, OpenaiChat and You">
136 <label class="file-label" for="image" title="Works with Bing, Gemini, OpenaiChat and You">
137 137 <input type="file" id="image" name="image" accept="image/*" required/>
138 138 <i class="fa-regular fa-image"></i>
139 139 </label>
140 <label for="camera">
140 <label class="file-label" for="camera">
141 141 <input type="file" id="camera" name="camera" accept="image/*" capture="camera" required/>
142 142 <i class="fa-solid fa-camera"></i>
143 143 </label>
144 <label for="file">
144 <label class="file-label" for="file">
145 145 <input type="file" id="file" name="file" accept="text/plain, text/html, text/xml, application/json, text/javascript, .sh, .py, .php, .css, .yaml, .sql, .log, .csv, .twig, .md" required/>
146 146 <i class="fa-solid fa-paperclip"></i>
147 147 </label>
Modified g4f/gui/client/static/css/style.css +6 -13
@@ -482,25 +482,18 @@ body {
482 482 display: none;
483 483 }
484 484
485 label[for="image"]:has(> input:valid){
486 color: var(--accent);
487 }
488
489 label[for="camera"]:has(> input:valid){
490 color: var(--accent);
491 }
492
493 label[for="file"]:has(> input:valid){
494 color: var(--accent);
495 }
496
497 label[for="image"], label[for="file"], label[for="camera"] {
485 .file-label {
498 486 cursor: pointer;
499 487 position: absolute;
500 488 top: 10px;
501 489 left: 10px;
502 490 }
503 491
492 .file-label:has(> input:valid),
493 .file-label.selected {
494 color: var(--accent);
495 }
496
504 497 label[for="image"] {
505 498 top: 32px;
506 499 }
Modified g4f/gui/client/static/js/chat.v1.js +51 -22
@@ -211,7 +211,7 @@ async function add_message_chunk(message) {
211 211 ${message.provider.model ? ' with ' + message.provider.model : ''}
212 212 `
213 213 } else if (message.type == "message") {
214 console.error(messag.message)
214 console.error(message.message)
215 215 } else if (message.type == "error") {
216 216 window.error = message.error
217 217 console.error(message.error);
@@ -240,6 +240,27 @@ async function add_message_chunk(message) {
240 240 }
241 241 }
242 242
243 cameraInput?.addEventListener("click", (e) => {
244 if (window?.pywebview) {
245 e.preventDefault();
246 pywebview.api.choose_file();
247 }
248 })
249
250 cameraInput?.addEventListener("click", (e) => {
251 if (window?.pywebview) {
252 e.preventDefault();
253 pywebview.api.take_picture();
254 }
255 })
256
257 imageInput?.addEventListener("click", (e) => {
258 if (window?.pywebview) {
259 e.preventDefault();
260 pywebview.api.choose_image();
261 }
262 })
263
243 264 const ask_gpt = async () => {
244 265 regenerate.classList.add(`regenerate-hidden`);
245 266 messages = await get_messages(window.conversation_id);
@@ -307,8 +328,7 @@ const ask_gpt = async () => {
307 328 console.error(e);
308 329 if (e.name != "AbortError") {
309 330 error = true;
310 text = "oops ! something went wrong, please try again / reload. [stacktrace in console]";
311 content_inner.innerHTML = text;
331 content_inner.innerHTML += `<p><strong>An error occured:</strong> ${e}</p>`;
312 332 }
313 333 }
314 334 if (!error && text) {
@@ -592,7 +612,7 @@ document.getElementById("cancelButton").addEventListener("click", async () => {
592 612 console.log(`aborted ${window.conversation_id}`);
593 613 });
594 614
595 document.getElementById(`regenerateButton`).addEventListener(`click`, async () => {
615 document.getElementById("regenerateButton").addEventListener("click", async () => {
596 616 prompt_lock = true;
597 617 await hide_last_message(window.conversation_id);
598 618 window.token = message_id();
@@ -622,14 +642,20 @@ const message_id = () => {
622 642 async function hide_sidebar() {
623 643 sidebar.classList.remove("shown");
624 644 sidebar_button.classList.remove("rotated");
645 if (window.location.pathname == "/menu/") {
646 history.back();
647 }
625 648 }
626 649
650 window.addEventListener('popstate', hide_sidebar, false);
651
627 652 sidebar_button.addEventListener("click", (event) => {
628 653 if (sidebar.classList.contains("shown")) {
629 654 hide_sidebar();
630 655 } else {
631 656 sidebar.classList.add("shown");
632 657 sidebar_button.classList.add("rotated");
658 history.pushState({}, null, "/menu/");
633 659 }
634 660 window.scrollTo(0, 0);
635 661 });
@@ -817,19 +843,6 @@ async function on_api() {
817 843
818 844 register_settings_storage();
819 845
820 versions = await api("version");
821 document.title = 'g4f - ' + versions["version"];
822 let text = "version ~ "
823 if (versions["version"] != versions["latest_version"]) {
824 let release_url = 'https://github.com/xtekky/gpt4free/releases/tag/' + versions["latest_version"];
825 let title = `New version: ${versions["latest_version"]}`;
826 text += `<a href="${release_url}" target="_blank" title="${title}">${versions["version"]}</a> `;
827 text += `<i class="fa-solid fa-rotate"></i>`
828 } else {
829 text += versions["version"];
830 }
831 document.getElementById("version_text").innerHTML = text
832
833 846 models = await api("models");
834 847 models.forEach((model) => {
835 848 let option = document.createElement("option");
@@ -845,8 +858,24 @@ async function on_api() {
845 858 })
846 859
847 860 await load_provider_models(appStorage.getItem("provider"));
848 load_settings_storage()
861 await load_settings_storage()
862 }
863
864 async function load_version() {
865 const versions = await api("version");
866 document.title = 'g4f - ' + versions["version"];
867 let text = "version ~ "
868 if (versions["version"] != versions["latest_version"]) {
869 let release_url = 'https://github.com/xtekky/gpt4free/releases/tag/' + versions["latest_version"];
870 let title = `New version: ${versions["latest_version"]}`;
871 text += `<a href="${release_url}" target="_blank" title="${title}">${versions["version"]}</a> `;
872 text += `<i class="fa-solid fa-rotate"></i>`
873 } else {
874 text += versions["version"];
875 }
876 document.getElementById("version_text").innerHTML = text
849 877 }
878 setTimeout(load_version, 5000);
850 879
851 880 for (const el of [imageInput, cameraInput]) {
852 881 el.addEventListener('click', async () => {
@@ -913,13 +942,13 @@ function get_selected_model() {
913 942
914 943 async function api(ressource, args=null, file=null) {
915 944 if (window?.pywebview) {
916 if (args) {
945 if (args !== null) {
917 946 if (ressource == "models") {
918 947 ressource = "provider_models";
919 948 }
920 return pywebview.api["get_" + ressource](args);
949 return pywebview.api[`get_${ressource}`](args);
921 950 }
922 return pywebview.api["get_" + ressource]();
951 return pywebview.api[`get_${ressource}`]();
923 952 }
924 953 if (ressource == "models" && args) {
925 954 ressource = `${ressource}/${args}`;
@@ -930,7 +959,7 @@ async function api(ressource, args=null, file=null) {
930 959 const headers = {
931 960 accept: 'text/event-stream'
932 961 }
933 if (file) {
962 if (file !== null) {
934 963 const formData = new FormData();
935 964 formData.append('file', file);
936 965 formData.append('json', body);
Added g4f/gui/server/android_gallery.py +67 -0
@@ -0,0 +1,67 @@
1 from kivy.logger import Logger
2 from kivy.clock import Clock
3
4 from jnius import autoclass
5 from jnius import cast
6 from android import activity
7
8 PythonActivity = autoclass('org.kivy.android.PythonActivity')
9 Intent = autoclass('android.content.Intent')
10 Uri = autoclass('android.net.Uri')
11
12 MEDIA_DATA = "_data"
13 RESULT_LOAD_IMAGE = 1
14
15 Activity = autoclass('android.app.Activity')
16
17 def user_select_image(on_selection):
18 """Open Gallery Activity and call callback with absolute image filepath of image user selected.
19 None if user canceled.
20 """
21
22 currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
23
24 # Forum discussion: https://groups.google.com/forum/#!msg/kivy-users/bjsG2j9bptI/-Oe_aGo0newJ
25 def on_activity_result(request_code, result_code, intent):
26 if request_code != RESULT_LOAD_IMAGE:
27 Logger.warning('user_select_image: ignoring activity result that was not RESULT_LOAD_IMAGE')
28 return
29
30 if result_code == Activity.RESULT_CANCELED:
31 Clock.schedule_once(lambda dt: on_selection(None), 0)
32 return
33
34 if result_code != Activity.RESULT_OK:
35 # This may just go into the void...
36 raise NotImplementedError('Unknown result_code "{}"'.format(result_code))
37
38 selectedImage = intent.getData(); # Uri
39 filePathColumn = [MEDIA_DATA]; # String[]
40 # Cursor
41 cursor = currentActivity.getContentResolver().query(selectedImage,
42 filePathColumn, None, None, None);
43 cursor.moveToFirst();
44
45 # int
46 columnIndex = cursor.getColumnIndex(filePathColumn[0]);
47 # String
48 picturePath = cursor.getString(columnIndex);
49 cursor.close();
50 Logger.info('android_ui: user_select_image() selected %s', picturePath)
51
52 # This is possibly in a different thread?
53 Clock.schedule_once(lambda dt: on_selection(picturePath), 0)
54
55 # See: http://pyjnius.readthedocs.org/en/latest/android.html
56 activity.bind(on_activity_result=on_activity_result)
57
58 intent = Intent()
59
60 # http://programmerguru.com/android-tutorial/how-to-pick-image-from-gallery/
61 # http://stackoverflow.com/questions/18416122/open-gallery-app-in-android
62 intent.setAction(Intent.ACTION_PICK)
63 # TODO internal vs external?
64 intent.setData(Uri.parse('content://media/internal/images/media'))
65 # TODO setType(Image)?
66
67 currentActivity.startActivityForResult(intent, RESULT_LOAD_IMAGE)
Modified g4f/gui/server/api.py +88 -1
@@ -1,11 +1,40 @@
1 from __future__ import annotations
2
1 3 import logging
2 4 import json
5 import os.path
3 6 from typing import Iterator
7 from uuid import uuid4
8 from functools import partial
4 9
5 10 try:
6 11 import webview
12 import platformdirs
7 13 except ImportError:
8 14 ...
15 try:
16 from plyer import camera
17 from plyer import filechooser
18 has_plyer = True
19 except ImportError:
20 has_plyer = False
21 try:
22 from android.runnable import run_on_ui_thread
23 from android.storage import app_storage_path
24 from android.permissions import request_permissions, Permission
25 from android.permissions import _RequestPermissionsManager
26 _RequestPermissionsManager.register_callback()
27 from .android_gallery import user_select_image
28 has_android = True
29 except ImportError:
30 run_on_ui_thread = lambda a : a
31 app_storage_path = platformdirs.user_pictures_dir
32 user_select_image = partial(
33 filechooser.open_file,
34 path=platformdirs.user_pictures_dir(),
35 filters=[["Image", "*.jpg", "*.jpeg", "*.png", "*.webp", "*.svg"]],
36 )
37 has_android = False
9 38
10 39 from g4f import version, models
11 40 from g4f import get_last_provider, ChatCompletion
@@ -75,13 +104,71 @@ class Api():
75 104 return {'title': ''}
76 105
77 106 def get_conversation(self, options: dict, **kwargs) -> Iterator:
78 window = webview.active_window()
107 window = webview.windows[0]
108 if hasattr(self, "image") and self.image is not None:
109 kwargs["image"] = open(self.image, "rb")
79 110 for message in self._create_response_stream(
80 111 self._prepare_conversation_kwargs(options, kwargs),
81 112 options.get("conversation_id")
82 113 ):
83 114 if not window.evaluate_js(f"if (!this.abort) this.add_message_chunk({json.dumps(message)}); !this.abort && !this.error;"):
84 115 break
116 self.image = None
117 self.set_selected(None)
118
119 @run_on_ui_thread
120 def choose_file(self):
121 self.request_permissions()
122 filechooser.open_file(
123 path=platformdirs.user_pictures_dir(),
124 on_selection=print
125 )
126
127 @run_on_ui_thread
128 def choose_image(self):
129 self.request_permissions()
130 user_select_image(
131 on_selection=self.on_image_selection
132 )
133
134 @run_on_ui_thread
135 def take_picture(self):
136 self.request_permissions()
137 filename = os.path.join(app_storage_path(), f"chat-{uuid4()}.png")
138 camera.take_picture(filename=filename, on_complete=self.on_camera)
139
140 def on_image_selection(self, filename):
141 if filename is not None and os.path.exists(filename):
142 self.image = filename
143 else:
144 self.image = None
145 self.set_selected(None if self.image is None else "image")
146
147 def on_camera(self, filename):
148 if filename is not None and os.path.exists(filename):
149 self.image = filename
150 else:
151 self.image = None
152 self.set_selected(None if self.image is None else "camera")
153
154 def set_selected(self, input_id: str = None):
155 window = webview.windows[0]
156 if window is not None:
157 window.evaluate_js(
158 f"document.querySelector(`.file-label.selected`)?.classList.remove(`selected`);"
159 )
160 if input_id is not None and input_id in ("image", "camera"):
161 window.evaluate_js(
162 f'document.querySelector(`label[for="{input_id}"]`)?.classList.add(`selected`);'
163 )
164
165 def request_permissions(self):
166 if has_android:
167 request_permissions([
168 Permission.CAMERA,
169 Permission.READ_EXTERNAL_STORAGE,
170 Permission.WRITE_EXTERNAL_STORAGE
171 ])
85 172
86 173 def _prepare_conversation_kwargs(self, json_data: dict, kwargs: dict):
87 174 """
Modified g4f/gui/webview.py +4 -0
@@ -1,3 +1,5 @@
1 from __future__ import annotations
2
1 3 import sys
2 4 import os.path
3 5 import webview
@@ -20,6 +22,8 @@ def run_webview(
20 22 dirname = sys._MEIPASS
21 23 else:
22 24 dirname = os.path.dirname(__file__)
25 webview.settings['OPEN_EXTERNAL_LINKS_IN_BROWSER'] = False
26 webview.settings['ALLOW_DOWNLOADS'] = True
23 27 webview.create_window(
24 28 f"g4f - {g4f.version.utils.current_version}",
25 29 os.path.join(dirname, "client/index.html"),
Modified requirements-min.txt +2 -1
@@ -1,2 +1,3 @@
1 1 requests
2 aiohttp
2 aiohttp
3 brotli
Modified setup.py +1 -0
@@ -11,6 +11,7 @@ with codecs.open(os.path.join(here, 'README.md'), encoding='utf-8') as fh:
11 11 INSTALL_REQUIRE = [
12 12 "requests",
13 13 "aiohttp",
14 "brotli"
14 15 ]
15 16
16 17 EXTRA_REQUIRE = {