Add new Client API with Docs

Use object urls for the preview of image uploads.
Fix upload images in You provider
Fix create image. It's now a single image.
Improve system message for create images.
This commit is contained in:
Heiner Lohaus 2024-02-12 11:41:27 +01:00
parent 9aeae65b9b
commit aba4b96f23
14 changed files with 480 additions and 125 deletions

View File

@ -24,7 +24,7 @@ jobs:
run: pip install -r requirements-min.txt run: pip install -r requirements-min.txt
- name: Run tests - name: Run tests
run: python -m etc.unittest run: python -m etc.unittest
- name: Set up Python 3.11 - name: Set up Python 3.12
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.12" python-version: "3.12"

View File

@ -226,6 +226,23 @@ docker-compose down
## 💡 Usage ## 💡 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 ### The Web UI
To start the web interface, type the following codes in the command line. To start the web interface, type the following codes in the command line.

BIN
docs/cat.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

71
docs/client.md Normal file
View File

@ -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)

View File

@ -1,60 +1,22 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import time
import os import os
from typing import Generator from typing import Generator
from ..cookies import get_cookies from ..cookies import get_cookies
from ..webdriver import WebDriver, get_driver_cookies, get_browser
from ..image import ImageResponse from ..image import ImageResponse
from ..errors import MissingRequirementsError, MissingAuthError 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" class BingCreateImages:
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:
"""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: dict[str, str] = {}, proxy: str = None) -> None:
self.cookies = cookies self.cookies = cookies
self.proxy = proxy 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. Generator for creating imagecompletion based on a prompt.
@ -91,4 +53,4 @@ class CreateImagesBing:
proxy = self.proxy or os.environ.get("G4F_PROXY") proxy = self.proxy or os.environ.get("G4F_PROXY")
async with create_session(cookies, proxy) as session: async with create_session(cookies, proxy) as session:
images = await create_images(session, prompt, proxy) images = await create_images(session, prompt, proxy)
return ImageResponse(images, prompt, {"preview": "{image}?w=200&h=200"}) return ImageResponse(images, prompt, {"preview": "{image}?w=200&h=200"} if len(images) > 1 else {})

View File

@ -58,9 +58,14 @@ class You(AsyncGeneratorProvider):
"selectedChatMode": chat_mode, "selectedChatMode": chat_mode,
#"chat": json.dumps(chat), #"chat": json.dumps(chat),
} }
params = {
"userFiles": upload,
"selectedChatMode": chat_mode,
}
async with (client.post if chat_mode == "default" else client.get)( async with (client.post if chat_mode == "default" else client.get)(
f"{cls.url}/api/streamingSearch", f"{cls.url}/api/streamingSearch",
data=data, data=data,
params=params,
headers=headers, headers=headers,
cookies=cookies cookies=cookies
) as response: ) as response:

View File

@ -53,7 +53,7 @@ from .Vercel import Vercel
from .Ylokh import Ylokh from .Ylokh import Ylokh
from .You import You from .You import You
from .CreateImagesBing import CreateImagesBing from .BingCreateImages import BingCreateImages
import sys import sys

View File

@ -21,8 +21,10 @@ from ..create_images import CreateImagesProvider
from ..helper import get_connector from ..helper import get_connector
from ...base_provider import ProviderType from ...base_provider import ProviderType
from ...errors import MissingRequirementsError from ...errors import MissingRequirementsError
from ...webdriver import WebDriver, get_driver_cookies, get_browser
BING_URL = "https://www.bing.com" BING_URL = "https://www.bing.com"
TIMEOUT_LOGIN = 1200
TIMEOUT_IMAGE_CREATION = 300 TIMEOUT_IMAGE_CREATION = 300
ERRORS = [ ERRORS = [
"this prompt is being reviewed", "this prompt is being reviewed",
@ -35,6 +37,39 @@ BAD_IMAGES = [
"https://r.bing.com/rp/TX9QuO3WzcCJz1uaaSwQAz39Kb0.jpg", "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: def create_session(cookies: Dict[str, str], proxy: str = None, connector: BaseConnector = None) -> ClientSession:
""" """
Creates a new client session with specified cookies and headers. 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") soup = BeautifulSoup(html_content, "html.parser")
tags = soup.find_all("img", class_="mimg") 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] images = [img["src"].split("?w=")[0] for img in tags]
if any(im in BAD_IMAGES for im in images): if any(im in BAD_IMAGES for im in images):
raise RuntimeError("Bad images found") raise RuntimeError("Bad images found")
@ -158,10 +195,10 @@ def patch_provider(provider: ProviderType) -> CreateImagesProvider:
Returns: Returns:
CreateImagesProvider: The patched provider with image creation capabilities. CreateImagesProvider: The patched provider with image creation capabilities.
""" """
from ..CreateImagesBing import CreateImagesBing from ..BingCreateImages import BingCreateImages
service = CreateImagesBing() service = BingCreateImages()
return CreateImagesProvider( return CreateImagesProvider(
provider, provider,
service.create_completion, service.create,
service.create_async service.create_async
) )

View File

@ -7,10 +7,14 @@ from ..typing import CreateResult, Messages
from ..base_provider import BaseProvider, ProviderType from ..base_provider import BaseProvider, ProviderType
system_message = """ 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: To generate an image with a prompt, do this:
<img data-prompt=\"keywords for the image\"> <img data-prompt=\"keywords for the image\">
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.
<img data-prompt=\"image caption\"> <img data-prompt=\"image caption\">
""" """

View File

@ -386,50 +386,47 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
) as response: ) as response:
if not response.ok: if not response.ok:
raise RuntimeError(f"Response {response.status_code}: {await response.text()}") raise RuntimeError(f"Response {response.status_code}: {await response.text()}")
try: last_message: int = 0
last_message: int = 0 async for line in response.iter_lines():
async for line in response.iter_lines(): if not line.startswith(b"data: "):
if not line.startswith(b"data: "): continue
continue elif line.startswith(b"data: [DONE]"):
elif line.startswith(b"data: [DONE]"): break
break try:
try: line = json.loads(line[6:])
line = json.loads(line[6:]) except:
except: continue
continue if "message" not in line:
if "message" not in line: continue
continue if "error" in line and line["error"]:
if "error" in line and line["error"]: raise RuntimeError(line["error"])
raise RuntimeError(line["error"]) if "message_type" not in line["message"]["metadata"]:
if "message_type" not in line["message"]["metadata"]: continue
continue try:
try: image_response = await cls.get_generated_image(session, auth_headers, line)
image_response = await cls.get_generated_image(session, auth_headers, line) if image_response:
if image_response: yield image_response
yield image_response except Exception as e:
except Exception as e: yield e
yield e if line["message"]["author"]["role"] != "assistant":
if line["message"]["author"]["role"] != "assistant": continue
continue if line["message"]["content"]["content_type"] != "text":
if line["message"]["content"]["content_type"] != "text": continue
continue if line["message"]["metadata"]["message_type"] not in ("next", "continue", "variant"):
if line["message"]["metadata"]["message_type"] not in ("next", "continue", "variant"): continue
continue conversation_id = line["conversation_id"]
conversation_id = line["conversation_id"] parent_id = line["message"]["id"]
parent_id = line["message"]["id"] if response_fields:
if response_fields: response_fields = False
response_fields = False yield ResponseFields(conversation_id, parent_id, end_turn)
yield ResponseFields(conversation_id, parent_id, end_turn) if "parts" in line["message"]["content"]:
if "parts" in line["message"]["content"]: new_message = line["message"]["content"]["parts"][0]
new_message = line["message"]["content"]["parts"][0] if len(new_message) > last_message:
if len(new_message) > last_message: yield new_message[last_message:]
yield new_message[last_message:] last_message = len(new_message)
last_message = len(new_message) if "finish_details" in line["message"]["metadata"]:
if "finish_details" in line["message"]["metadata"]: if line["message"]["metadata"]["finish_details"]["type"] == "stop":
if line["message"]["metadata"]["finish_details"]["type"] == "stop": end_turn.end()
end_turn.end()
except Exception as e:
raise e
if not auto_continue: if not auto_continue:
break break
action = "continue" action = "continue"

View File

@ -16,7 +16,8 @@ def get_model_and_provider(model : Union[Model, str],
stream : bool, stream : bool,
ignored : list[str] = None, ignored : list[str] = None,
ignore_working: bool = False, 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. Retrieves the model and provider based on input parameters.

267
g4f/client.py Normal file
View File

@ -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<code>[\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()

View File

@ -1,35 +1,38 @@
class ProviderNotFoundError(Exception): class ProviderNotFoundError(Exception):
pass ...
class ProviderNotWorkingError(Exception): class ProviderNotWorkingError(Exception):
pass ...
class StreamNotSupportedError(Exception): class StreamNotSupportedError(Exception):
pass ...
class ModelNotFoundError(Exception): class ModelNotFoundError(Exception):
pass ...
class ModelNotAllowedError(Exception): class ModelNotAllowedError(Exception):
pass ...
class RetryProviderError(Exception): class RetryProviderError(Exception):
pass ...
class RetryNoProviderError(Exception): class RetryNoProviderError(Exception):
pass ...
class VersionNotFoundError(Exception): class VersionNotFoundError(Exception):
pass ...
class NestAsyncioError(Exception): class NestAsyncioError(Exception):
pass ...
class ModelNotSupportedError(Exception): class ModelNotSupportedError(Exception):
pass ...
class MissingRequirementsError(Exception): class MissingRequirementsError(Exception):
pass ...
class MissingAuthError(Exception): class MissingAuthError(Exception):
pass ...
class NoImageResponseError(Exception):
...

View File

@ -52,6 +52,12 @@ const handle_ask = async () => {
} }
await add_message(window.conversation_id, "user", message); await add_message(window.conversation_id, "user", message);
window.token = message_id(); 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 += ` message_box.innerHTML += `
<div class="message"> <div class="message">
<div class="user"> <div class="user">
@ -64,10 +70,6 @@ const handle_ask = async () => {
? '<img src="' + imageInput.dataset.src + '" alt="Image upload">' ? '<img src="' + imageInput.dataset.src + '" alt="Image upload">'
: '' : ''
} }
${cameraInput.dataset.src
? '<img src="' + cameraInput.dataset.src + '" alt="Image capture">'
: ''
}
</div> </div>
</div> </div>
`; `;
@ -683,24 +685,13 @@ observer.observe(message_input, { attributes: true });
document.getElementById("version_text").innerHTML = text document.getElementById("version_text").innerHTML = text
})() })()
for (const el of [imageInput, cameraInput]) { for (const el of [imageInput, cameraInput]) {
console.log(el.files);
el.addEventListener('click', async () => { el.addEventListener('click', async () => {
el.value = ''; el.value = '';
delete el.dataset.src; if (imageInput.dataset.src) {
}); URL.revokeObjectURL(imageInput.dataset.src);
do_load = async () => { delete imageInput.dataset.src
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]);
} }
} });
do_load()
el.addEventListener('change', do_load);
} }
fileInput.addEventListener('click', async (event) => { fileInput.addEventListener('click', async (event) => {
fileInput.value = ''; fileInput.value = '';