Merge pull request #1684 from hlohaus/retry

Add count chars to gui, Add retry support to fix rate limit in Bing
This commit is contained in:
H Lohaus 2024-03-13 15:17:25 +01:00 committed by GitHub
commit 9c381f2906
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 223 additions and 153 deletions

View File

@ -4,16 +4,20 @@ import random
import json import json
import uuid import uuid
import time import time
import asyncio
from urllib import parse from urllib import parse
from aiohttp import ClientSession, ClientTimeout, BaseConnector from datetime import datetime
from aiohttp import ClientSession, ClientTimeout, BaseConnector, WSMsgType
from ..typing import AsyncResult, Messages, ImageType, Cookies from ..typing import AsyncResult, Messages, ImageType, Cookies
from ..image import ImageResponse, ImageRequest from ..image import ImageRequest
from ..errors import ResponseStatusError
from .base_provider import AsyncGeneratorProvider from .base_provider import AsyncGeneratorProvider
from .helper import get_connector from .helper import get_connector, get_random_hex
from .bing.upload_image import upload_image from .bing.upload_image import upload_image
from .bing.create_images import create_images
from .bing.conversation import Conversation, create_conversation, delete_conversation from .bing.conversation import Conversation, create_conversation, delete_conversation
from .BingCreateImages import BingCreateImages
from .. import debug
class Tones: class Tones:
""" """
@ -65,11 +69,14 @@ class Bing(AsyncGeneratorProvider):
prompt = messages[-1]["content"] prompt = messages[-1]["content"]
context = create_context(messages[:-1]) context = create_context(messages[:-1])
cookies = {**get_default_cookies(), **cookies} if cookies else get_default_cookies()
gpt4_turbo = True if model.startswith("gpt-4-turbo") else False gpt4_turbo = True if model.startswith("gpt-4-turbo") else False
return stream_generate(prompt, tone, image, context, cookies, get_connector(connector, proxy, True), web_search, gpt4_turbo, timeout) return stream_generate(
prompt, tone, image, context, cookies,
get_connector(connector, proxy, True),
proxy, web_search, gpt4_turbo, timeout,
**kwargs
)
def create_context(messages: Messages) -> str: def create_context(messages: Messages) -> str:
""" """
@ -78,14 +85,32 @@ def create_context(messages: Messages) -> str:
:param messages: A list of message dictionaries. :param messages: A list of message dictionaries.
:return: A string representing the context created from the messages. :return: A string representing the context created from the messages.
""" """
return "\n\n".join( return "".join(
f"[{message['role']}]" + ("(#message)" if message['role'] != "system" else "(#additional_instructions)") + f"\n{message['content']}" f"[{message['role']}]" + ("(#message)" if message['role'] != "system" else "(#additional_instructions)") + f"\n{message['content']}"
for message in messages for message in messages
) ) + "\n\n"
def get_ip_address() -> str: def get_ip_address() -> str:
return f"13.{random.randint(104, 107)}.{random.randint(0, 255)}.{random.randint(0, 255)}" return f"13.{random.randint(104, 107)}.{random.randint(0, 255)}.{random.randint(0, 255)}"
def get_default_cookies():
return {
'SRCHD' : 'AF=NOFORM',
'PPLState' : '1',
'KievRPSSecAuth': '',
'SUID' : '',
'SRCHUSR' : '',
'SRCHHPGUSR' : f'HV={int(time.time())}',
}
def create_headers(cookies: Cookies = None) -> dict:
if cookies is None:
cookies = get_default_cookies()
headers = Defaults.headers.copy()
headers["cookie"] = "; ".join(f"{k}={v}" for k, v in cookies.items())
headers["x-forwarded-for"] = get_ip_address()
return headers
class Defaults: class Defaults:
""" """
Default settings and configurations for the Bing provider. Default settings and configurations for the Bing provider.
@ -169,37 +194,26 @@ class Defaults:
} }
# Default headers for requests # Default headers for requests
home = 'https://www.bing.com/chat?q=Bing+AI&FORM=hpcodx'
headers = { headers = {
'accept': '*/*', 'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
'accept-language': 'en-US,en;q=0.9',
'cache-control': 'max-age=0',
'sec-ch-ua': '"Chromium";v="110", "Not A(Brand";v="24", "Microsoft Edge";v="110"',
'sec-ch-ua-arch': '"x86"',
'sec-ch-ua-bitness': '"64"',
'sec-ch-ua-full-version': '"110.0.1587.69"',
'sec-ch-ua-full-version-list': '"Chromium";v="110.0.5481.192", "Not A(Brand";v="24.0.0.0", "Microsoft Edge";v="110.0.1587.69"',
'sec-ch-ua-mobile': '?0', 'sec-ch-ua-mobile': '?0',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
'sec-ch-ua-arch': '"x86"',
'sec-ch-ua-full-version': '"122.0.6261.69"',
'accept': 'application/json',
'sec-ch-ua-platform-version': '"15.0.0"',
"x-ms-client-request-id": str(uuid.uuid4()),
'sec-ch-ua-full-version-list': '"Chromium";v="122.0.6261.69", "Not(A:Brand";v="24.0.0.0", "Google Chrome";v="122.0.6261.69"',
'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.12.3 OS/Windows',
'sec-ch-ua-model': '""', 'sec-ch-ua-model': '""',
'sec-ch-ua-platform': '"Windows"', 'sec-ch-ua-platform': '"Windows"',
'sec-ch-ua-platform-version': '"15.0.0"', 'sec-fetch-site': 'same-origin',
'sec-fetch-dest': 'document', 'sec-fetch-mode': 'cors',
'sec-fetch-mode': 'navigate', 'sec-fetch-dest': 'empty',
'sec-fetch-site': 'none', 'referer': home,
'sec-fetch-user': '?1', 'accept-encoding': 'gzip, deflate, br',
'upgrade-insecure-requests': '1', 'accept-language': 'en-US,en;q=0.9',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.69',
'x-edge-shopping-flag': '1',
'x-forwarded-for': get_ip_address(),
}
def get_default_cookies():
return {
'SRCHD' : 'AF=NOFORM',
'PPLState' : '1',
'KievRPSSecAuth': '',
'SUID' : '',
'SRCHUSR' : '',
'SRCHHPGUSR' : f'HV={int(time.time())}',
} }
def format_message(msg: dict) -> str: def format_message(msg: dict) -> str:
@ -234,8 +248,6 @@ def create_message(
""" """
options_sets = [] options_sets = []
if not web_search:
options_sets.append("nosearchall")
if gpt4_turbo: if gpt4_turbo:
options_sets.append("dlgpt4t") options_sets.append("dlgpt4t")
@ -249,7 +261,7 @@ def create_message(
"verbosity": "verbose", "verbosity": "verbose",
"scenario": "SERP", "scenario": "SERP",
"plugins": [{"id": "c310c353-b9f0-4d76-ab0d-1dd5e979cf68", "category": 1}] if web_search else [], "plugins": [{"id": "c310c353-b9f0-4d76-ab0d-1dd5e979cf68", "category": 1}] if web_search else [],
"traceId": str(uuid.uuid4()), "traceId": get_random_hex(40),
"conversationHistoryOptionsSets": ["autosave","savemem","uprofupd","uprofgen"], "conversationHistoryOptionsSets": ["autosave","savemem","uprofupd","uprofgen"],
"gptId": "copilot", "gptId": "copilot",
"isStartOfSession": True, "isStartOfSession": True,
@ -257,7 +269,7 @@ def create_message(
"message":{ "message":{
**Defaults.location, **Defaults.location,
"userIpAddress": get_ip_address(), "userIpAddress": get_ip_address(),
"timestamp": "2024-03-11T22:40:36+01:00", "timestamp": datetime.now().isoformat(),
"author": "user", "author": "user",
"inputMethod": "Keyboard", "inputMethod": "Keyboard",
"text": prompt, "text": prompt,
@ -266,6 +278,7 @@ def create_message(
"messageId": request_id "messageId": request_id
}, },
"tone": tone, "tone": tone,
"extraExtensionParameters": {"gpt-creator-persona": {"personaId": "copilot"}},
"spokenTextMode": "None", "spokenTextMode": "None",
"conversationId": conversation.conversationId, "conversationId": conversation.conversationId,
"participant": {"id": conversation.clientId} "participant": {"id": conversation.clientId}
@ -299,9 +312,15 @@ async def stream_generate(
context: str = None, context: str = None,
cookies: dict = None, cookies: dict = None,
connector: BaseConnector = None, connector: BaseConnector = None,
proxy: str = None,
web_search: bool = False, web_search: bool = False,
gpt4_turbo: bool = False, gpt4_turbo: bool = False,
timeout: int = 900 timeout: int = 900,
conversation: Conversation = None,
raise_apology: bool = False,
max_retries: int = 5,
sleep_retry: int = 15,
**kwargs
): ):
""" """
Asynchronously streams generated responses from the Bing API. Asynchronously streams generated responses from the Bing API.
@ -316,20 +335,30 @@ async def stream_generate(
:param timeout: Timeout for the request. :param timeout: Timeout for the request.
:return: An asynchronous generator yielding responses. :return: An asynchronous generator yielding responses.
""" """
headers = Defaults.headers headers = create_headers(cookies)
if cookies:
headers["cookie"] = "; ".join(f"{k}={v}" for k, v in cookies.items())
async with ClientSession( async with ClientSession(
headers=headers, cookies=cookies,
timeout=ClientTimeout(total=timeout), connector=connector timeout=ClientTimeout(total=timeout), connector=connector
) as session: ) as session:
conversation = await create_conversation(session) while conversation is None:
image_request = await upload_image(session, image, tone) if image else None do_read = True
try: try:
conversation = await create_conversation(session, headers)
except ResponseStatusError as e:
max_retries -= 1
if max_retries < 1:
raise e
if debug.logging:
print(f"Bing: Retry: {e}")
headers = create_headers()
await asyncio.sleep(sleep_retry)
continue
image_request = await upload_image(session, image, tone, headers) if image else None
async with session.ws_connect( async with session.ws_connect(
'wss://sydney.bing.com/sydney/ChatHub', 'wss://sydney.bing.com/sydney/ChatHub',
autoping=False, autoping=False,
params={'sec_access_token': conversation.conversationSignature} params={'sec_access_token': conversation.conversationSignature},
headers=headers
) as wss: ) as wss:
await wss.send_str(format_message({'protocol': 'json', 'version': 1})) await wss.send_str(format_message({'protocol': 'json', 'version': 1}))
await wss.send_str(format_message({"type": 6})) await wss.send_str(format_message({"type": 6}))
@ -337,11 +366,12 @@ async def stream_generate(
await wss.send_str(create_message(conversation, prompt, tone, context, image_request, web_search, gpt4_turbo)) await wss.send_str(create_message(conversation, prompt, tone, context, image_request, web_search, gpt4_turbo))
response_txt = '' response_txt = ''
returned_text = '' returned_text = ''
final = False
message_id = None message_id = None
while not final: while do_read:
msg = await wss.receive(timeout=timeout) msg = await wss.receive(timeout=timeout)
if not msg.data: if msg.type == WSMsgType.CLOSED:
break
if msg.type != WSMsgType.TEXT or not msg.data:
continue continue
objects = msg.data.split(Defaults.delimiter) objects = msg.data.split(Defaults.delimiter)
for obj in objects: for obj in objects:
@ -350,26 +380,27 @@ async def stream_generate(
response = json.loads(obj) response = json.loads(obj)
if response and response.get('type') == 1 and response['arguments'][0].get('messages'): if response and response.get('type') == 1 and response['arguments'][0].get('messages'):
message = response['arguments'][0]['messages'][0] message = response['arguments'][0]['messages'][0]
# Reset memory, if we have a new message
if message_id is not None and message_id != message["messageId"]: if message_id is not None and message_id != message["messageId"]:
returned_text = '' returned_text = ''
message_id = message["messageId"] message_id = message["messageId"]
image_response = None image_response = None
if (message['contentOrigin'] != 'Apology'): if (raise_apology and message['contentOrigin'] == 'Apology'):
if 'adaptiveCards' in message: raise RuntimeError("Apology Response Error")
card = message['adaptiveCards'][0]['body'][0] if 'adaptiveCards' in message:
if "text" in card: card = message['adaptiveCards'][0]['body'][0]
response_txt = card.get('text') if "text" in card:
if message.get('messageType') and "inlines" in card: response_txt = card.get('text')
inline_txt = card['inlines'][0].get('text') if message.get('messageType') and "inlines" in card:
response_txt += inline_txt + '\n' inline_txt = card['inlines'][0].get('text')
elif message.get('contentType') == "IMAGE": response_txt += inline_txt + '\n'
prompt = message.get('text') elif message.get('contentType') == "IMAGE":
try: prompt = message.get('text')
image_response = ImageResponse(await create_images(session, prompt), prompt, {"preview": "{image}?w=200&h=200"}) try:
except: image_client = BingCreateImages(cookies, proxy)
response_txt += f"\nhttps://www.bing.com/images/create?q={parse.quote(prompt)}" image_response = await image_client.create_async(prompt)
final = True except Exception as e:
response_txt += f"\nhttps://www.bing.com/images/create?q={parse.quote(prompt)}"
do_read = False
if response_txt.startswith(returned_text): if response_txt.startswith(returned_text):
new = response_txt[len(returned_text):] new = response_txt[len(returned_text):]
if new != "\n": if new != "\n":
@ -380,10 +411,18 @@ async def stream_generate(
elif response.get('type') == 2: elif response.get('type') == 2:
result = response['item']['result'] result = response['item']['result']
if result.get('error'): if result.get('error'):
if result["value"] == "CaptchaChallenge": max_retries -= 1
raise Exception(f"{result['value']}: Use other cookies or/and ip address") if max_retries < 1:
else: if result["value"] == "CaptchaChallenge":
raise Exception(f"{result['value']}: {result['message']}") raise RuntimeError(f"{result['value']}: Use other cookies or/and ip address")
else:
raise RuntimeError(f"{result['value']}: {result['message']}")
if debug.logging:
print(f"Bing: Retry: {result['value']}: {result['message']}")
headers = create_headers()
do_read = False
conversation = None
await asyncio.sleep(sleep_retry)
break
return return
finally: await delete_conversation(session, conversation, headers)
await delete_conversation(session, conversation)

View File

@ -7,14 +7,15 @@ from typing import Iterator, Union
from ..cookies import get_cookies from ..cookies import get_cookies
from ..image import ImageResponse from ..image import ImageResponse
from ..errors import MissingRequirementsError, MissingAuthError from ..errors import MissingRequirementsError, MissingAuthError
from ..typing import Cookies
from .bing.create_images import create_images, create_session, get_cookies_from_browser from .bing.create_images import create_images, create_session, get_cookies_from_browser
class BingCreateImages: class BingCreateImages:
"""A class for creating images using Bing.""" """A class for creating images using Bing."""
def __init__(self, cookies: dict[str, str] = {}, proxy: str = None) -> None: def __init__(self, cookies: Cookies = None, proxy: str = None) -> None:
self.cookies = cookies self.cookies: Cookies = cookies
self.proxy = proxy self.proxy: str = proxy
def create(self, prompt: str) -> Iterator[Union[ImageResponse, str]]: def create(self, prompt: str) -> Iterator[Union[ImageResponse, str]]:
""" """

View File

@ -1,8 +1,6 @@
from __future__ import annotations from __future__ import annotations
import uuid
from aiohttp import ClientSession from aiohttp import ClientSession
from ...errors import ResponseStatusError
from ...requests import raise_for_status from ...requests import raise_for_status
class Conversation: class Conversation:
@ -22,7 +20,7 @@ class Conversation:
self.clientId = clientId self.clientId = clientId
self.conversationSignature = conversationSignature self.conversationSignature = conversationSignature
async def create_conversation(session: ClientSession, proxy: str = None) -> Conversation: async def create_conversation(session: ClientSession, headers: dict) -> Conversation:
""" """
Create a new conversation asynchronously. Create a new conversation asynchronously.
@ -33,33 +31,15 @@ async def create_conversation(session: ClientSession, proxy: str = None) -> Conv
Returns: Returns:
Conversation: An instance representing the created conversation. Conversation: An instance representing the created conversation.
""" """
url = 'https://www.bing.com/search?toncp=0&FORM=hpcodx&q=Bing+AI&showconv=1&cc=en' url = "https://www.bing.com/turing/conversation/create?bundleVersion=1.1626.1"
headers = {
"cookie": "; ".join(f"{c.key}={c.value}" for c in session.cookie_jar)
}
async with session.get(url, headers=headers) as response: async with session.get(url, headers=headers) as response:
await raise_for_status(response) await raise_for_status(response, "Failed to create conversation")
headers = {
"accept": "application/json",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-ms-client-request-id": str(uuid.uuid4()),
"x-ms-useragent": "azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.12.3 OS/Windows",
"referer": "https://www.bing.com/search?toncp=0&FORM=hpcodx&q=Bing+AI&showconv=1&cc=en",
"cookie": "; ".join(f"{c.key}={c.value}" for c in session.cookie_jar)
}
url = "https://www.bing.com/turing/conversation/create?bundleVersion=1.1634.0-service-contracts"
async with session.get(url, headers=headers, proxy=proxy) as response:
if response.status == 404:
raise ResponseStatusError(f"Response {response.status}: Can't create a new chat")
await raise_for_status(response)
data = await response.json() data = await response.json()
conversationId = data.get('conversationId') conversationId = data.get('conversationId')
clientId = data.get('clientId') clientId = data.get('clientId')
conversationSignature = response.headers.get('X-Sydney-Encryptedconversationsignature') conversationSignature = response.headers.get('X-Sydney-Encryptedconversationsignature')
if not conversationId or not clientId or not conversationSignature: if not conversationId or not clientId or not conversationSignature:
raise Exception('Failed to create conversation.') raise RuntimeError('Empty fields: Failed to create conversation')
return Conversation(conversationId, clientId, conversationSignature) return Conversation(conversationId, clientId, conversationSignature)
async def list_conversations(session: ClientSession) -> list: async def list_conversations(session: ClientSession) -> list:
@ -76,8 +56,8 @@ async def list_conversations(session: ClientSession) -> list:
async with session.get(url) as response: async with session.get(url) as response:
response = await response.json() response = await response.json()
return response["chats"] return response["chats"]
async def delete_conversation(session: ClientSession, conversation: Conversation, proxy: str = None) -> bool: async def delete_conversation(session: ClientSession, conversation: Conversation, headers: dict) -> bool:
""" """
Delete a conversation asynchronously. Delete a conversation asynchronously.
@ -98,7 +78,7 @@ async def delete_conversation(session: ClientSession, conversation: Conversation
"optionsSets": ["autosave"] "optionsSets": ["autosave"]
} }
try: try:
async with session.post(url, json=json, proxy=proxy) as response: async with session.post(url, json=json, headers=headers) as response:
response = await response.json() response = await response.json()
return response["result"]["value"] == "Success" return response["result"]["value"] == "Success"
except: except:

View File

@ -9,6 +9,7 @@ from aiohttp import ClientSession, FormData
from ...typing import ImageType, Tuple from ...typing import ImageType, Tuple
from ...image import to_image, process_image, to_base64_jpg, ImageRequest, Image from ...image import to_image, process_image, to_base64_jpg, ImageRequest, Image
from ...requests import raise_for_status
IMAGE_CONFIG = { IMAGE_CONFIG = {
"maxImagePixels": 360000, "maxImagePixels": 360000,
@ -20,7 +21,7 @@ async def upload_image(
session: ClientSession, session: ClientSession,
image_data: ImageType, image_data: ImageType,
tone: str, tone: str,
proxy: str = None headers: dict
) -> ImageRequest: ) -> ImageRequest:
""" """
Uploads an image to Bing's AI service and returns the image response. Uploads an image to Bing's AI service and returns the image response.
@ -43,11 +44,9 @@ async def upload_image(
img_binary_data = to_base64_jpg(image, IMAGE_CONFIG['imageCompressionRate']) img_binary_data = to_base64_jpg(image, IMAGE_CONFIG['imageCompressionRate'])
data = build_image_upload_payload(img_binary_data, tone) data = build_image_upload_payload(img_binary_data, tone)
headers = prepare_headers(session)
async with session.post("https://www.bing.com/images/kblob", data=data, headers=headers, proxy=proxy) as response: async with session.post("https://www.bing.com/images/kblob", data=data, headers=prepare_headers(headers)) as response:
if response.status != 200: await raise_for_status(response, "Failed to upload image")
raise RuntimeError("Failed to upload image.")
return parse_image_response(await response.json()) return parse_image_response(await response.json())
def calculate_new_dimensions(image: Image) -> Tuple[int, int]: def calculate_new_dimensions(image: Image) -> Tuple[int, int]:
@ -109,7 +108,7 @@ def build_knowledge_request(tone: str) -> dict:
} }
} }
def prepare_headers(session: ClientSession) -> dict: def prepare_headers(headers: dict) -> dict:
""" """
Prepares the headers for the image upload request. Prepares the headers for the image upload request.
@ -120,7 +119,6 @@ def prepare_headers(session: ClientSession) -> dict:
Returns: Returns:
dict: The headers for the request. dict: The headers for the request.
""" """
headers = session.headers.copy()
headers["Referer"] = 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx' headers["Referer"] = 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx'
headers["Origin"] = 'https://www.bing.com' headers["Origin"] = 'https://www.bing.com'
return headers return headers

View File

@ -40,7 +40,7 @@ def get_cookies(domain_name: str = '', raise_requirements_error: bool = True, si
""" """
if domain_name in _cookies: if domain_name in _cookies:
return _cookies[domain_name] return _cookies[domain_name]
cookies = load_cookies_from_browsers(domain_name, raise_requirements_error, single_browser) cookies = load_cookies_from_browsers(domain_name, raise_requirements_error, single_browser)
_cookies[domain_name] = cookies _cookies[domain_name] = cookies
return cookies return cookies

View File

@ -189,11 +189,13 @@
<label for="switch" title="Add the pages of the first 5 search results to the query."></label> <label for="switch" title="Add the pages of the first 5 search results to the query."></label>
<span class="about">Web Access</span> <span class="about">Web Access</span>
</div> </div>
<!--
<div class="field"> <div class="field">
<input type="checkbox" id="patch" /> <input type="checkbox" id="patch" />
<label for="patch" title="Enable create images with Bing."></label> <label for="patch" title="Enable create images with Bing."></label>
<span class="about">Image Generator</span> <span class="about">Image Generator</span>
</div> </div>
-->
<div class="field"> <div class="field">
<input type="checkbox" id="history" /> <input type="checkbox" id="history" />
<label for="history" title="To improve the reaction time or if you have trouble with large conversations."></label> <label for="history" title="To improve the reaction time or if you have trouble with large conversations."></label>

View File

@ -27,6 +27,13 @@ messageInput.addEventListener("focus", () => {
document.documentElement.scrollTop = document.documentElement.scrollHeight; document.documentElement.scrollTop = document.documentElement.scrollHeight;
}); });
appStorage = window.localStorage || {
setItem: (key, value) => self[key] = value,
getItem: (key) => self[key],
removeItem: (key) => delete self[key],
length: 0
}
const markdown_render = (content) => { const markdown_render = (content) => {
return markdown.render(content return markdown.render(content
.replaceAll(/<!--.+-->/gm, "") .replaceAll(/<!--.+-->/gm, "")
@ -67,10 +74,10 @@ const register_remove_message = async () => {
} }
const delete_conversations = async () => { const delete_conversations = async () => {
for (let i = 0; i < localStorage.length; i++){ for (let i = 0; i < appStorage.length; i++){
let key = localStorage.key(i); let key = appStorage.key(i);
if (key.startsWith("conversation:")) { if (key.startsWith("conversation:")) {
localStorage.removeItem(key); appStorage.removeItem(key);
} }
} }
hide_sidebar(); hide_sidebar();
@ -238,7 +245,7 @@ const ask_gpt = async () => {
jailbreak: jailbreak.options[jailbreak.selectedIndex].value, jailbreak: jailbreak.options[jailbreak.selectedIndex].value,
web_search: document.getElementById(`switch`).checked, web_search: document.getElementById(`switch`).checked,
provider: provider.options[provider.selectedIndex].value, provider: provider.options[provider.selectedIndex].value,
patch_provider: document.getElementById('patch').checked, patch_provider: document.getElementById('patch')?.checked,
messages: messages messages: messages
}); });
const headers = { const headers = {
@ -261,6 +268,7 @@ const ask_gpt = async () => {
body: body body: body
}); });
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = ""
while (true) { while (true) {
const { value, done } = await reader.read(); const { value, done } = await reader.read();
if (done) break; if (done) break;
@ -268,7 +276,14 @@ const ask_gpt = async () => {
if (!line) { if (!line) {
continue; continue;
} }
const message = JSON.parse(line); let message;
try {
message = JSON.parse(buffer + line);
buffer = "";
} catch {
buffer += line
continue;
}
if (message.type == "content") { if (message.type == "content") {
text += message.content; text += message.content;
} else if (message.type == "provider") { } else if (message.type == "provider") {
@ -389,7 +404,7 @@ const hide_option = async (conversation_id) => {
}; };
const delete_conversation = async (conversation_id) => { const delete_conversation = async (conversation_id) => {
localStorage.removeItem(`conversation:${conversation_id}`); appStorage.removeItem(`conversation:${conversation_id}`);
const conversation = document.getElementById(`convo-${conversation_id}`); const conversation = document.getElementById(`convo-${conversation_id}`);
conversation.remove(); conversation.remove();
@ -491,13 +506,13 @@ const load_conversation = async (conversation_id, scroll = true) => {
async function get_conversation(conversation_id) { async function get_conversation(conversation_id) {
let conversation = await JSON.parse( let conversation = await JSON.parse(
localStorage.getItem(`conversation:${conversation_id}`) appStorage.getItem(`conversation:${conversation_id}`)
); );
return conversation; return conversation;
} }
async function save_conversation(conversation_id, conversation) { async function save_conversation(conversation_id, conversation) {
localStorage.setItem( appStorage.setItem(
`conversation:${conversation_id}`, `conversation:${conversation_id}`,
JSON.stringify(conversation) JSON.stringify(conversation)
); );
@ -515,7 +530,7 @@ async function add_conversation(conversation_id, content) {
title = content + '&nbsp;'.repeat(19 - content.length) title = content + '&nbsp;'.repeat(19 - content.length)
} }
if (localStorage.getItem(`conversation:${conversation_id}`) == null) { if (appStorage.getItem(`conversation:${conversation_id}`) == null) {
await save_conversation(conversation_id, { await save_conversation(conversation_id, {
id: conversation_id, id: conversation_id,
title: title, title: title,
@ -577,9 +592,9 @@ const add_message = async (conversation_id, role, content, provider) => {
const load_conversations = async () => { const load_conversations = async () => {
let conversations = []; let conversations = [];
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < appStorage.length; i++) {
if (localStorage.key(i).startsWith("conversation:")) { if (appStorage.key(i).startsWith("conversation:")) {
let conversation = localStorage.getItem(localStorage.key(i)); let conversation = appStorage.getItem(appStorage.key(i));
conversations.push(JSON.parse(conversation)); conversations.push(JSON.parse(conversation));
} }
} }
@ -654,13 +669,16 @@ sidebar_button.addEventListener("click", (event) => {
const register_settings_localstorage = async () => { const register_settings_localstorage = async () => {
for (id of ["switch", "model", "jailbreak", "patch", "provider", "history"]) { for (id of ["switch", "model", "jailbreak", "patch", "provider", "history"]) {
element = document.getElementById(id); element = document.getElementById(id);
if (!element) {
continue;
}
element.addEventListener('change', async (event) => { element.addEventListener('change', async (event) => {
switch (event.target.type) { switch (event.target.type) {
case "checkbox": case "checkbox":
localStorage.setItem(id, event.target.checked); appStorage.setItem(id, event.target.checked);
break; break;
case "select-one": case "select-one":
localStorage.setItem(id, event.target.selectedIndex); appStorage.setItem(id, event.target.selectedIndex);
break; break;
default: default:
console.warn("Unresolved element type"); console.warn("Unresolved element type");
@ -672,7 +690,9 @@ const register_settings_localstorage = async () => {
const load_settings_localstorage = async () => { const load_settings_localstorage = async () => {
for (id of ["switch", "model", "jailbreak", "patch", "provider", "history"]) { for (id of ["switch", "model", "jailbreak", "patch", "provider", "history"]) {
element = document.getElementById(id); element = document.getElementById(id);
value = localStorage.getItem(element.id); if (!element || !(value = appStorage.getItem(element.id))) {
continue;
}
if (value) { if (value) {
switch (element.type) { switch (element.type) {
case "checkbox": case "checkbox":
@ -712,12 +732,12 @@ const say_hello = async () => {
// Theme storage for recurring viewers // Theme storage for recurring viewers
const storeTheme = function (theme) { const storeTheme = function (theme) {
localStorage.setItem("theme", theme); appStorage.setItem("theme", theme);
}; };
// set theme when visitor returns // set theme when visitor returns
const setTheme = function () { const setTheme = function () {
const activeTheme = localStorage.getItem("theme"); const activeTheme = appStorage.getItem("theme");
colorThemes.forEach((themeOption) => { colorThemes.forEach((themeOption) => {
if (themeOption.id === activeTheme) { if (themeOption.id === activeTheme) {
themeOption.checked = true; themeOption.checked = true;
@ -751,8 +771,12 @@ function count_words(text) {
return text.trim().match(/[\w\u4E00-\u9FA5]+/gu)?.length || 0; return text.trim().match(/[\w\u4E00-\u9FA5]+/gu)?.length || 0;
} }
function count_chars(text) {
return text.match(/[^\s\p{P}]/gu)?.length || 0;
}
function count_words_and_tokens(text, model) { function count_words_and_tokens(text, model) {
return `(${count_words(text)} words, ${count_tokens(model, text)} tokens)`; return `(${count_words(text)} words, ${count_chars(text)} chars, ${count_tokens(model, text)} tokens)`;
} }
let countFocus = messageInput; let countFocus = messageInput;

24
g4f/gui/webview.py Normal file
View File

@ -0,0 +1,24 @@
import webview
from functools import partial
from platformdirs import user_config_dir
from g4f.gui import run_gui
from g4f.gui.run import gui_parser
import g4f.version
import g4f.debug
def run_webview(host: str = "0.0.0.0", port: int = 8080, debug: bool = True):
webview.create_window(f"g4f - {g4f.version.utils.current_version}", f"http://{host}:{port}/")
if debug:
g4f.debug.logging = True
webview.start(
partial(run_gui, host, port),
private_mode=False,
storage_path=user_config_dir("g4f-webview"),
debug=debug
)
if __name__ == "__main__":
parser = gui_parser()
args = parser.parse_args()
run_webview(args.host, args.port, args.debug)

View File

@ -6,6 +6,7 @@ import asyncio
from .. import debug from .. import debug
from ..typing import CreateResult, Messages from ..typing import CreateResult, Messages
from .types import BaseProvider, ProviderType from .types import BaseProvider, ProviderType
from ..image import ImageResponse
system_message = """ system_message = """
You can generate images, pictures, photos or img with the DALL-E 3 image generator. You can generate images, pictures, photos or img with the DALL-E 3 image generator.
@ -92,7 +93,9 @@ class CreateImagesProvider(BaseProvider):
messages.insert(0, {"role": "system", "content": self.system_message}) messages.insert(0, {"role": "system", "content": self.system_message})
buffer = "" buffer = ""
for chunk in self.provider.create_completion(model, messages, stream, **kwargs): for chunk in self.provider.create_completion(model, messages, stream, **kwargs):
if isinstance(chunk, str) and buffer or "<" in chunk: if isinstance(chunk, ImageResponse):
yield chunk
elif isinstance(chunk, str) and buffer or "<" in chunk:
buffer += chunk buffer += chunk
if ">" in buffer: if ">" in buffer:
match = re.search(r'<img data-prompt="(.*?)">', buffer) match = re.search(r'<img data-prompt="(.*?)">', buffer)

View File

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import random import random
import secrets
import string import string
from ..typing import Messages from ..typing import Messages
@ -40,11 +39,14 @@ def get_random_string(length: int = 10) -> str:
for _ in range(length) for _ in range(length)
) )
def get_random_hex() -> str: def get_random_hex(length: int = 32) -> str:
""" """
Generate a random hexadecimal string of a fixed length. Generate a random hexadecimal string with n length.
Returns: Returns:
str: A random hexadecimal string of 32 characters (16 bytes). str: A random hexadecimal string of n characters.
""" """
return secrets.token_hex(16).zfill(32) return ''.join(
random.choice("abcdef" + string.digits)
for _ in range(length)
)

View File

@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
from urllib.parse import urlparse
from typing import Union from typing import Union
from aiohttp import ClientResponse from aiohttp import ClientResponse
from requests import Response as RequestsResponse from requests import Response as RequestsResponse
@ -15,7 +14,7 @@ except ImportError:
has_curl_cffi = False has_curl_cffi = False
from ..webdriver import WebDriver, WebDriverSession from ..webdriver import WebDriver, WebDriverSession
from ..webdriver import user_config_dir, bypass_cloudflare, get_driver_cookies from ..webdriver import bypass_cloudflare, get_driver_cookies
from ..errors import MissingRequirementsError, RateLimitError, ResponseStatusError from ..errors import MissingRequirementsError, RateLimitError, ResponseStatusError
from .defaults import DEFAULT_HEADERS from .defaults import DEFAULT_HEADERS
@ -39,17 +38,16 @@ def get_args_from_browser(
Returns: Returns:
Session: A Session object configured with cookies and headers from the WebDriver. Session: A Session object configured with cookies and headers from the WebDriver.
""" """
user_data_dir = "" #user_config_dir(f"g4f-{urlparse(url).hostname}") with WebDriverSession(webdriver, "", proxy=proxy, virtual_display=virtual_display) as driver:
with WebDriverSession(webdriver, user_data_dir, proxy=proxy, virtual_display=virtual_display) as driver:
if do_bypass_cloudflare: if do_bypass_cloudflare:
bypass_cloudflare(driver, url, timeout) bypass_cloudflare(driver, url, timeout)
user_agent = driver.execute_script("return navigator.userAgent")
headers = { headers = {
**DEFAULT_HEADERS, **DEFAULT_HEADERS,
'referer': url, 'referer': url,
'user-agent': user_agent,
} }
if hasattr(driver, "requests"): if not hasattr(driver, "requests"):
headers["user-agent"] = driver.execute_script("return navigator.userAgent")
else:
for request in driver.requests: for request in driver.requests:
if request.url.startswith(url): if request.url.startswith(url):
for key, value in request.headers.items(): for key, value in request.headers.items():
@ -83,22 +81,22 @@ def get_session_from_browser(url: str, webdriver: WebDriver = None, proxy: str =
impersonate="chrome" impersonate="chrome"
) )
async def raise_for_status_async(response: Union[StreamResponse, ClientResponse]): async def raise_for_status_async(response: Union[StreamResponse, ClientResponse], message: str = None):
if response.status in (429, 402): if response.status in (429, 402):
raise RateLimitError(f"Response {response.status}: Rate limit reached") raise RateLimitError(f"Response {response.status}: Rate limit reached")
text = await response.text() if not response.ok else None message = await response.text() if not response.ok and message is None else message
if response.status == 403 and "<title>Just a moment...</title>" in text: if response.status == 403 and "<title>Just a moment...</title>" in message:
raise ResponseStatusError(f"Response {response.status}: Cloudflare detected") raise ResponseStatusError(f"Response {response.status}: Cloudflare detected")
elif not response.ok: elif not response.ok:
raise ResponseStatusError(f"Response {response.status}: {text}") raise ResponseStatusError(f"Response {response.status}: {message}")
def raise_for_status(response: Union[StreamResponse, ClientResponse, Response, RequestsResponse]): def raise_for_status(response: Union[StreamResponse, ClientResponse, Response, RequestsResponse], message: str = None):
if isinstance(response, StreamSession) or isinstance(response, ClientResponse): if isinstance(response, StreamSession) or isinstance(response, ClientResponse):
return raise_for_status_async(response) return raise_for_status_async(response, message)
if response.status_code in (429, 402): if response.status_code in (429, 402):
raise RateLimitError(f"Response {response.status_code}: Rate limit reached") raise RateLimitError(f"Response {response.status_code}: Rate limit reached")
elif response.status_code == 403 and "<title>Just a moment...</title>" in response.text: elif response.status_code == 403 and "<title>Just a moment...</title>" in response.text:
raise ResponseStatusError(f"Response {response.status_code}: Cloudflare detected") raise ResponseStatusError(f"Response {response.status_code}: Cloudflare detected")
elif not response.ok: elif not response.ok:
raise ResponseStatusError(f"Response {response.status_code}: {response.text}") raise ResponseStatusError(f"Response {response.status_code}: {response.text if message is None else message}")

View File

@ -10,10 +10,9 @@ try:
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import NoSuchElementException
from undetected_chromedriver import Chrome, ChromeOptions
has_requirements = True has_requirements = True
except ImportError: except ImportError:
from typing import Type as WebDriver, Callable as user_config_dir from typing import Type as WebDriver
has_requirements = False has_requirements = False
import time import time
@ -38,9 +37,9 @@ try:
def __init__(self, *args, options=None, seleniumwire_options={}, **kwargs): def __init__(self, *args, options=None, seleniumwire_options={}, **kwargs):
if options is None: if options is None:
options = ChromeOptions() options = ChromeOptions()
options.add_argument('--proxy-bypass-list=<-loopback>')
config = self._setup_backend(seleniumwire_options) config = self._setup_backend(seleniumwire_options)
options.add_argument(f"--proxy-server={config['proxy']['httpProxy']}") options.add_argument(f"--proxy-server={config['proxy']['httpProxy']}")
options.add_argument('--proxy-bypass-list=<-loopback>')
options.add_argument("--ignore-certificate-errors") options.add_argument("--ignore-certificate-errors")
super().__init__(*args, options=options, **kwargs) super().__init__(*args, options=options, **kwargs)
has_seleniumwire = True has_seleniumwire = True