diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 96e274b1..f49235b6 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -24,7 +24,7 @@ jobs: run: pip install -r requirements-min.txt - name: Run tests run: python -m etc.unittest - - name: Set up Python 3.11 + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: python-version: "3.12" diff --git a/README.md b/README.md index 6734fe02..dc7ef933 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,23 @@ docker-compose down ## 💡 Usage +### New Client with Image Generation +```python +from g4f.client import Client + +client = Client() +response = client.images.generate( + model="gemini", + prompt="a white siamese cat", + ... +) +image_url = response.data[0].url +``` +Result: +[![Image with cat](/docs/cat.jpeg)](/docs/client.md) + +[to the client API](/docs/client.md) + ### The Web UI To start the web interface, type the following codes in the command line. diff --git a/docs/cat.jpeg b/docs/cat.jpeg new file mode 100644 index 00000000..56bbb159 Binary files /dev/null and b/docs/cat.jpeg differ diff --git a/docs/client.md b/docs/client.md new file mode 100644 index 00000000..1201a83d --- /dev/null +++ b/docs/client.md @@ -0,0 +1,71 @@ +### Client API +##### from g4f (beta) + +#### Start +This new client could: + +```python +from g4f.client import Client +``` +replaces this: + +```python +from openai import OpenAI +``` +in your Python Code. + +New client have the same API as OpenAI. + +#### Client + +Create the client with custom providers: + +```python +from g4f.client import Client +from g4f.Provider import BingCreateImages, OpenaiChat, Gemini + +client = Client( + provider=OpenaiChat, + image_provider=Gemini, + proxies=None +) +``` + +#### Examples + +Use the ChatCompletions: + +```python +stream = client.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": "Say this is a test"}], + stream=True, +) +for chunk in stream: + if chunk.choices[0].delta.content is not None: + print(chunk.choices[0].delta.content, end="") +``` + +Or use it for creating a image: +```python +response = client.images.generate( + model="dall-e-3", + prompt="a white siamese cat", + ... +) + +image_url = response.data[0].url +``` + +Also this works with the client: +```python +response = client.images.create_variation( + image=open('cat.jpg') + model="bing", + ... +) + +image_url = response.data[0].url +``` + +[to Home](/docs/client.md) diff --git a/g4f/Provider/CreateImagesBing.py b/g4f/Provider/BingCreateImages.py similarity index 59% rename from g4f/Provider/CreateImagesBing.py rename to g4f/Provider/BingCreateImages.py index 59eafe0c..901b7787 100644 --- a/g4f/Provider/CreateImagesBing.py +++ b/g4f/Provider/BingCreateImages.py @@ -1,60 +1,22 @@ from __future__ import annotations import asyncio -import time import os from typing import Generator from ..cookies import get_cookies -from ..webdriver import WebDriver, get_driver_cookies, get_browser from ..image import ImageResponse from ..errors import MissingRequirementsError, MissingAuthError -from .bing.create_images import BING_URL, create_images, create_session +from .bing.create_images import create_images, create_session, get_cookies_from_browser -BING_URL = "https://www.bing.com" -TIMEOUT_LOGIN = 1200 - -def wait_for_login(driver: WebDriver, timeout: int = TIMEOUT_LOGIN) -> None: - """ - Waits for the user to log in within a given timeout period. - - Args: - driver (WebDriver): Webdriver for browser automation. - timeout (int): Maximum waiting time in seconds. - - Raises: - RuntimeError: If the login process exceeds the timeout. - """ - driver.get(f"{BING_URL}/") - start_time = time.time() - while not driver.get_cookie("_U"): - if time.time() - start_time > timeout: - raise RuntimeError("Timeout error") - time.sleep(0.5) - -def get_cookies_from_browser(proxy: str = None) -> dict[str, str]: - """ - Retrieves cookies from the browser using webdriver. - - Args: - proxy (str, optional): Proxy configuration. - - Returns: - dict[str, str]: Retrieved cookies. - """ - with get_browser(proxy=proxy) as driver: - wait_for_login(driver) - time.sleep(1) - return get_driver_cookies(driver) - -class CreateImagesBing: +class BingCreateImages: """A class for creating images using Bing.""" def __init__(self, cookies: dict[str, str] = {}, proxy: str = None) -> None: self.cookies = cookies self.proxy = proxy - def create_completion(self, prompt: str) -> Generator[ImageResponse, None, None]: + def create(self, prompt: str) -> Generator[ImageResponse, None, None]: """ Generator for creating imagecompletion based on a prompt. @@ -91,4 +53,4 @@ class CreateImagesBing: proxy = self.proxy or os.environ.get("G4F_PROXY") async with create_session(cookies, proxy) as session: images = await create_images(session, prompt, proxy) - return ImageResponse(images, prompt, {"preview": "{image}?w=200&h=200"}) \ No newline at end of file + return ImageResponse(images, prompt, {"preview": "{image}?w=200&h=200"} if len(images) > 1 else {}) \ No newline at end of file diff --git a/g4f/Provider/You.py b/g4f/Provider/You.py index 001f775d..ece1d340 100644 --- a/g4f/Provider/You.py +++ b/g4f/Provider/You.py @@ -58,9 +58,14 @@ class You(AsyncGeneratorProvider): "selectedChatMode": chat_mode, #"chat": json.dumps(chat), } + params = { + "userFiles": upload, + "selectedChatMode": chat_mode, + } async with (client.post if chat_mode == "default" else client.get)( f"{cls.url}/api/streamingSearch", data=data, + params=params, headers=headers, cookies=cookies ) as response: diff --git a/g4f/Provider/__init__.py b/g4f/Provider/__init__.py index 7dbc1504..68b62fd9 100644 --- a/g4f/Provider/__init__.py +++ b/g4f/Provider/__init__.py @@ -53,7 +53,7 @@ from .Vercel import Vercel from .Ylokh import Ylokh from .You import You -from .CreateImagesBing import CreateImagesBing +from .BingCreateImages import BingCreateImages import sys diff --git a/g4f/Provider/bing/create_images.py b/g4f/Provider/bing/create_images.py index de4fd476..7b82dc56 100644 --- a/g4f/Provider/bing/create_images.py +++ b/g4f/Provider/bing/create_images.py @@ -21,8 +21,10 @@ from ..create_images import CreateImagesProvider from ..helper import get_connector from ...base_provider import ProviderType from ...errors import MissingRequirementsError +from ...webdriver import WebDriver, get_driver_cookies, get_browser BING_URL = "https://www.bing.com" +TIMEOUT_LOGIN = 1200 TIMEOUT_IMAGE_CREATION = 300 ERRORS = [ "this prompt is being reviewed", @@ -35,6 +37,39 @@ BAD_IMAGES = [ "https://r.bing.com/rp/TX9QuO3WzcCJz1uaaSwQAz39Kb0.jpg", ] +def wait_for_login(driver: WebDriver, timeout: int = TIMEOUT_LOGIN) -> None: + """ + Waits for the user to log in within a given timeout period. + + Args: + driver (WebDriver): Webdriver for browser automation. + timeout (int): Maximum waiting time in seconds. + + Raises: + RuntimeError: If the login process exceeds the timeout. + """ + driver.get(f"{BING_URL}/") + start_time = time.time() + while not driver.get_cookie("_U"): + if time.time() - start_time > timeout: + raise RuntimeError("Timeout error") + time.sleep(0.5) + +def get_cookies_from_browser(proxy: str = None) -> dict[str, str]: + """ + Retrieves cookies from the browser using webdriver. + + Args: + proxy (str, optional): Proxy configuration. + + Returns: + dict[str, str]: Retrieved cookies. + """ + with get_browser(proxy=proxy) as driver: + wait_for_login(driver) + time.sleep(1) + return get_driver_cookies(driver) + def create_session(cookies: Dict[str, str], proxy: str = None, connector: BaseConnector = None) -> ClientSession: """ Creates a new client session with specified cookies and headers. @@ -141,6 +176,8 @@ def read_images(html_content: str) -> List[str]: """ soup = BeautifulSoup(html_content, "html.parser") tags = soup.find_all("img", class_="mimg") + if not tags: + tags = soup.find_all("img", class_="gir_mmimg") images = [img["src"].split("?w=")[0] for img in tags] if any(im in BAD_IMAGES for im in images): raise RuntimeError("Bad images found") @@ -158,10 +195,10 @@ def patch_provider(provider: ProviderType) -> CreateImagesProvider: Returns: CreateImagesProvider: The patched provider with image creation capabilities. """ - from ..CreateImagesBing import CreateImagesBing - service = CreateImagesBing() + from ..BingCreateImages import BingCreateImages + service = BingCreateImages() return CreateImagesProvider( provider, - service.create_completion, + service.create, service.create_async ) \ No newline at end of file diff --git a/g4f/Provider/create_images.py b/g4f/Provider/create_images.py index 9a9e3f08..2ca92432 100644 --- a/g4f/Provider/create_images.py +++ b/g4f/Provider/create_images.py @@ -7,10 +7,14 @@ from ..typing import CreateResult, Messages from ..base_provider import BaseProvider, ProviderType system_message = """ -You can generate custom images with the DALL-E 3 image generator. +You can generate images, pictures, photos or img with the DALL-E 3 image generator. To generate an image with a prompt, do this: + -Don't use images with data uri. It is important to use a prompt instead. + +Never use own image links. Don't wrap it in backticks. +It is important to use a only a img tag with a prompt. + """ diff --git a/g4f/Provider/needs_auth/OpenaiChat.py b/g4f/Provider/needs_auth/OpenaiChat.py index b1b267fd..9e0edd8a 100644 --- a/g4f/Provider/needs_auth/OpenaiChat.py +++ b/g4f/Provider/needs_auth/OpenaiChat.py @@ -386,50 +386,47 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): ) as response: if not response.ok: raise RuntimeError(f"Response {response.status_code}: {await response.text()}") - try: - last_message: int = 0 - async for line in response.iter_lines(): - if not line.startswith(b"data: "): - continue - elif line.startswith(b"data: [DONE]"): - break - try: - line = json.loads(line[6:]) - except: - continue - if "message" not in line: - continue - if "error" in line and line["error"]: - raise RuntimeError(line["error"]) - if "message_type" not in line["message"]["metadata"]: - continue - try: - image_response = await cls.get_generated_image(session, auth_headers, line) - if image_response: - yield image_response - except Exception as e: - yield e - if line["message"]["author"]["role"] != "assistant": - continue - if line["message"]["content"]["content_type"] != "text": - continue - if line["message"]["metadata"]["message_type"] not in ("next", "continue", "variant"): - continue - conversation_id = line["conversation_id"] - parent_id = line["message"]["id"] - if response_fields: - response_fields = False - yield ResponseFields(conversation_id, parent_id, end_turn) - if "parts" in line["message"]["content"]: - new_message = line["message"]["content"]["parts"][0] - if len(new_message) > last_message: - yield new_message[last_message:] - last_message = len(new_message) - if "finish_details" in line["message"]["metadata"]: - if line["message"]["metadata"]["finish_details"]["type"] == "stop": - end_turn.end() - except Exception as e: - raise e + last_message: int = 0 + async for line in response.iter_lines(): + if not line.startswith(b"data: "): + continue + elif line.startswith(b"data: [DONE]"): + break + try: + line = json.loads(line[6:]) + except: + continue + if "message" not in line: + continue + if "error" in line and line["error"]: + raise RuntimeError(line["error"]) + if "message_type" not in line["message"]["metadata"]: + continue + try: + image_response = await cls.get_generated_image(session, auth_headers, line) + if image_response: + yield image_response + except Exception as e: + yield e + if line["message"]["author"]["role"] != "assistant": + continue + if line["message"]["content"]["content_type"] != "text": + continue + if line["message"]["metadata"]["message_type"] not in ("next", "continue", "variant"): + continue + conversation_id = line["conversation_id"] + parent_id = line["message"]["id"] + if response_fields: + response_fields = False + yield ResponseFields(conversation_id, parent_id, end_turn) + if "parts" in line["message"]["content"]: + new_message = line["message"]["content"]["parts"][0] + if len(new_message) > last_message: + yield new_message[last_message:] + last_message = len(new_message) + if "finish_details" in line["message"]["metadata"]: + if line["message"]["metadata"]["finish_details"]["type"] == "stop": + end_turn.end() if not auto_continue: break action = "continue" diff --git a/g4f/__init__.py b/g4f/__init__.py index 93e4aa86..ec4a1743 100644 --- a/g4f/__init__.py +++ b/g4f/__init__.py @@ -16,7 +16,8 @@ def get_model_and_provider(model : Union[Model, str], stream : bool, ignored : list[str] = None, ignore_working: bool = False, - ignore_stream: bool = False) -> tuple[str, ProviderType]: + ignore_stream: bool = False, + **kwargs) -> tuple[str, ProviderType]: """ Retrieves the model and provider based on input parameters. diff --git a/g4f/client.py b/g4f/client.py new file mode 100644 index 00000000..117db375 --- /dev/null +++ b/g4f/client.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +import re + +from .typing import Union, Generator, AsyncGenerator, Messages, ImageType +from .base_provider import BaseProvider, ProviderType +from .Provider.base_provider import AsyncGeneratorProvider +from .image import ImageResponse as ImageProviderResponse +from .Provider import BingCreateImages, Gemini, OpenaiChat +from .errors import NoImageResponseError +from . import get_model_and_provider + +ImageProvider = Union[BaseProvider, object] +Proxies = Union[dict, str] + +def read_json(text: str) -> dict: + """ + Parses JSON code block from a string. + + Args: + text (str): A string containing a JSON code block. + + Returns: + dict: A dictionary parsed from the JSON code block. + """ + match = re.search(r"```(json|)\n(?P[\S\s]+?)\n```", text) + if match: + return match.group("code") + return text + +def iter_response( + response: iter, + stream: bool, + response_format: dict = None, + max_tokens: int = None, + stop: list = None +) -> Generator: + content = "" + idx = 1 + chunk = None + finish_reason = "stop" + for idx, chunk in enumerate(response): + content += str(chunk) + if max_tokens is not None and idx > max_tokens: + finish_reason = "max_tokens" + break + first = -1 + word = None + if stop is not None: + for word in list(stop): + first = content.find(word) + if first != -1: + content = content[:first] + break + if stream: + if first != -1: + first = chunk.find(word) + if first != -1: + chunk = chunk[:first] + else: + first = 0 + yield ChatCompletionChunk([ChatCompletionDeltaChoice(ChatCompletionDelta(chunk))]) + if first != -1: + break + if not stream: + if response_format is not None and "type" in response_format: + if response_format["type"] == "json_object": + response = read_json(response) + yield ChatCompletion([ChatCompletionChoice(ChatCompletionMessage(response, finish_reason))]) + +async def aiter_response( + response: aiter, + stream: bool, + response_format: dict = None, + max_tokens: int = None, + stop: list = None +) -> AsyncGenerator: + content = "" + try: + idx = 0 + chunk = None + async for chunk in response: + content += str(chunk) + if max_tokens is not None and idx > max_tokens: + break + first = -1 + word = None + if stop is not None: + for word in list(stop): + first = content.find(word) + if first != -1: + content = content[:first] + break + if stream: + if first != -1: + first = chunk.find(word) + if first != -1: + chunk = chunk[:first] + else: + first = 0 + yield ChatCompletionChunk([ChatCompletionDeltaChoice(ChatCompletionDelta(chunk))]) + if first != -1: + break + idx += 1 + except: + ... + if not stream: + if response_format is not None and "type" in response_format: + if response_format["type"] == "json_object": + response = read_json(response) + yield ChatCompletion([ChatCompletionChoice(ChatCompletionMessage(response))]) + +class Model(): + def __getitem__(self, item): + return getattr(self, item) + +class ChatCompletion(Model): + def __init__(self, choices: list): + self.choices = choices + +class ChatCompletionChunk(Model): + def __init__(self, choices: list): + self.choices = choices + +class ChatCompletionChoice(Model): + def __init__(self, message: ChatCompletionMessage): + self.message = message + +class ChatCompletionMessage(Model): + def __init__(self, content: str, finish_reason: str): + self.content = content + self.finish_reason = finish_reason + self.index = 0 + self.logprobs = None + +class ChatCompletionDelta(Model): + def __init__(self, content: str): + self.content = content + +class ChatCompletionDeltaChoice(Model): + def __init__(self, delta: ChatCompletionDelta): + self.delta = delta + +class Client(): + proxies: Proxies = None + chat: Chat + + def __init__( + self, + provider: ProviderType = None, + image_provider: ImageProvider = None, + proxies: Proxies = None, + **kwargs + ) -> None: + self.proxies: Proxies = proxies + self.images = Images(self, image_provider) + self.chat = Chat(self, provider) + + def get_proxy(self) -> Union[str, None]: + if isinstance(self.proxies, str) or self.proxies is None: + return self.proxies + elif "all" in self.proxies: + return self.proxies["all"] + elif "https" in self.proxies: + return self.proxies["https"] + return None + +class Completions(): + def __init__(self, client: Client, provider: ProviderType = None): + self.client: Client = client + self.provider: ProviderType = provider + + def create( + self, + messages: Messages, + model: str, + provider: ProviderType = None, + stream: bool = False, + response_format: dict = None, + max_tokens: int = None, + stop: list = None, + **kwargs + ) -> Union[dict, Generator]: + if max_tokens is not None: + kwargs["max_tokens"] = max_tokens + if stop: + kwargs["stop"] = list(stop) + model, provider = get_model_and_provider( + model, + self.provider if provider is None else provider, + stream, + **kwargs + ) + response = provider.create_completion(model, messages, stream=stream, **kwargs) + if isinstance(provider, type) and issubclass(provider, AsyncGeneratorProvider): + response = iter_response(response, stream, response_format) # max_tokens, stop + else: + response = iter_response(response, stream, response_format, max_tokens, stop) + return response if stream else next(response) + +class Chat(): + completions: Completions + + def __init__(self, client: Client, provider: ProviderType = None): + self.completions = Completions(client, provider) + +class ImageModels(): + gemini = Gemini + openai = OpenaiChat + + def __init__(self, client: Client) -> None: + self.client = client + self.default = BingCreateImages(proxy=self.client.get_proxy()) + + def get(self, name: str) -> ImageProvider: + return getattr(self, name) if hasattr(self, name) else self.default + +class ImagesResponse(Model): + data: list[Image] + + def __init__(self, data: list) -> None: + self.data = data + +class Image(Model): + url: str + + def __init__(self, url: str) -> None: + self.url = url + +class Images(): + def __init__(self, client: Client, provider: ImageProvider = None): + self.client: Client = client + self.provider: ImageProvider = provider + self.models: ImageModels = ImageModels(client) + + def generate(self, prompt, model: str = None, **kwargs): + provider = self.models.get(model) if model else self.provider or self.models.get(model) + if isinstance(provider, BaseProvider) or isinstance(provider, type) and issubclass(provider, BaseProvider): + prompt = f"create a image: {prompt}" + response = provider.create_completion( + "", + [{"role": "user", "content": prompt}], + True, + proxy=self.client.get_proxy() + ) + else: + response = provider.create(prompt) + + for chunk in response: + if isinstance(chunk, ImageProviderResponse): + return ImagesResponse([Image(image)for image in list(chunk.images)]) + raise NoImageResponseError() + + def create_variation(self, image: ImageType, model: str = None, **kwargs): + provider = self.models.get(model) if model else self.provider + if isinstance(provider, BaseProvider): + response = provider.create_completion( + "", + [{"role": "user", "content": "create a image like this"}], + True, + image=image, + proxy=self.client.get_proxy() + ) + for chunk in response: + if isinstance(chunk, ImageProviderResponse): + return ImagesResponse([Image(image)for image in list(chunk.images)]) + raise NoImageResponseError() \ No newline at end of file diff --git a/g4f/errors.py b/g4f/errors.py index 48171a6e..5e821ded 100644 --- a/g4f/errors.py +++ b/g4f/errors.py @@ -1,35 +1,38 @@ class ProviderNotFoundError(Exception): - pass + ... class ProviderNotWorkingError(Exception): - pass + ... class StreamNotSupportedError(Exception): - pass + ... class ModelNotFoundError(Exception): - pass + ... class ModelNotAllowedError(Exception): - pass + ... class RetryProviderError(Exception): - pass + ... class RetryNoProviderError(Exception): - pass + ... class VersionNotFoundError(Exception): - pass + ... class NestAsyncioError(Exception): - pass + ... class ModelNotSupportedError(Exception): - pass + ... class MissingRequirementsError(Exception): - pass + ... class MissingAuthError(Exception): - pass \ No newline at end of file + ... + +class NoImageResponseError(Exception): + ... \ No newline at end of file diff --git a/g4f/gui/client/js/chat.v1.js b/g4f/gui/client/js/chat.v1.js index a243b1de..2042d174 100644 --- a/g4f/gui/client/js/chat.v1.js +++ b/g4f/gui/client/js/chat.v1.js @@ -52,6 +52,12 @@ const handle_ask = async () => { } await add_message(window.conversation_id, "user", message); window.token = message_id(); + + if (imageInput.dataset.src) URL.revokeObjectURL(imageInput.dataset.src); + const input = imageInput && imageInput.files.length > 0 ? imageInput : cameraInput + if (input.files.length > 0) imageInput.dataset.src = URL.createObjectURL(input.files[0]); + else delete imageInput.dataset.src + message_box.innerHTML += `
@@ -64,10 +70,6 @@ const handle_ask = async () => { ? 'Image upload' : '' } - ${cameraInput.dataset.src - ? 'Image capture' - : '' - }
`; @@ -683,24 +685,13 @@ observer.observe(message_input, { attributes: true }); document.getElementById("version_text").innerHTML = text })() for (const el of [imageInput, cameraInput]) { - console.log(el.files); el.addEventListener('click', async () => { el.value = ''; - delete el.dataset.src; - }); - do_load = async () => { - if (el.files.length) { - delete imageInput.dataset.src; - delete cameraInput.dataset.src; - const reader = new FileReader(); - reader.addEventListener('load', (event) => { - el.dataset.src = event.target.result; - }); - reader.readAsDataURL(el.files[0]); + if (imageInput.dataset.src) { + URL.revokeObjectURL(imageInput.dataset.src); + delete imageInput.dataset.src } - } - do_load() - el.addEventListener('change', do_load); + }); } fileInput.addEventListener('click', async (event) => { fileInput.value = '';