├── requirements.txt ├── gemini_client ├── __init__.py ├── enums.py ├── utils.py ├── images.py └── core.py ├── LICENSE ├── setup.py ├── gemini.js └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | curl_cffi>=0.5.9 2 | pydantic>=2.0 3 | rich>=10.0 4 | requests>=2.20 5 | -------------------------------------------------------------------------------- /gemini_client/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .core import Chatbot, AsyncChatbot 3 | from .enums import Model, Endpoint, Headers 4 | from .images import Image, WebImage, GeneratedImage 5 | from .utils import upload_file, load_cookies 6 | 7 | __all__ = [ 8 | "Chatbot", 9 | "AsyncChatbot", 10 | "Model", 11 | "Endpoint", 12 | "Headers", 13 | "Image", 14 | "WebImage", 15 | "GeneratedImage", 16 | "upload_file", 17 | "load_cookies", 18 | ] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 OEvortex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name="gemini-client-api", 8 | version="0.1.0", 9 | author="AI Agent Contributor", # Generic author 10 | author_email="contributor@example.com", # Generic email 11 | description="A Python client for interacting with Google's Gemini API using curl_cffi.", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/example/gemini-client-api", # Generic repository URL 15 | packages=find_packages(where=".", include=["gemini_client", "gemini_client.*"]), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | "Intended Audience :: Developers", 21 | "Topic :: Internet :: WWW/HTTP", 22 | "Topic :: Software Development :: Libraries :: Python Modules", 23 | ], 24 | python_requires=">=3.7", # Based on f-strings, asyncio, pydantic v2 features 25 | install_requires=[ 26 | "curl_cffi>=0.5.9", 27 | "pydantic>=2.0", 28 | "rich>=10.0", 29 | "requests>=2.20", 30 | ], 31 | keywords="gemini google ai api client curl_cffi async", 32 | project_urls={ 33 | "Bug Reports": "https://github.com/example/gemini-client-api/issues", # Generic issue URL 34 | "Source": "https://github.com/example/gemini-client-api", # Generic source URL 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /gemini_client/enums.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from enum import Enum 3 | 4 | class Endpoint(Enum): 5 | """ 6 | Enum for Google Gemini API endpoints. 7 | 8 | Attributes: 9 | INIT (str): URL for initializing the Gemini session. 10 | GENERATE (str): URL for generating chat responses. 11 | ROTATE_COOKIES (str): URL for rotating authentication cookies. 12 | UPLOAD (str): URL for uploading files/images. 13 | """ 14 | INIT = "https://gemini.google.com/app" 15 | GENERATE = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate" 16 | ROTATE_COOKIES = "https://accounts.google.com/RotateCookies" 17 | UPLOAD = "https://content-push.googleapis.com/upload" 18 | 19 | class Headers(Enum): 20 | """ 21 | Enum for HTTP headers used in Gemini API requests. 22 | 23 | Attributes: 24 | GEMINI (dict): Headers for Gemini chat requests. 25 | ROTATE_COOKIES (dict): Headers for rotating cookies. 26 | UPLOAD (dict): Headers for file/image upload. 27 | """ 28 | GEMINI = { 29 | "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", 30 | "Host": "gemini.google.com", 31 | "Origin": "https://gemini.google.com", 32 | "Referer": "https://gemini.google.com/", 33 | # User-Agent will be handled by curl_cffi impersonate 34 | # "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", 35 | "X-Same-Domain": "1", 36 | } 37 | ROTATE_COOKIES = { 38 | "Content-Type": "application/json", 39 | } 40 | UPLOAD = {"Push-ID": "feeds/mcudyrk2a4khkz"} 41 | 42 | class Model(Enum): 43 | """ 44 | Enum for available Gemini model configurations. 45 | 46 | Attributes: 47 | model_name (str): Name of the model. 48 | model_header (dict): Additional headers required for the model. 49 | advanced_only (bool): Whether the model is available only for advanced users. 50 | """ 51 | # Updated model definitions based on reference implementation 52 | UNSPECIFIED = ("unspecified", {}, False) 53 | G_2_0_FLASH = ( 54 | "gemini-2.0-flash", 55 | {"x-goog-ext-525001261-jspb": '[1,null,null,null,"f299729663a2343f"]'}, 56 | False, 57 | ) 58 | G_2_0_FLASH_THINKING = ( 59 | "gemini-2.0-flash-thinking", 60 | {"x-goog-ext-525001261-jspb": '[null,null,null,null,"7ca48d02d802f20a"]'}, 61 | False, 62 | ) 63 | G_2_5_FLASH = ( 64 | "gemini-2.5-flash", 65 | {"x-goog-ext-525001261-jspb": '[1,null,null,null,"35609594dbe934d8"]'}, 66 | False, 67 | ) 68 | G_2_5_PRO = ( 69 | "gemini-2.5-pro", 70 | {"x-goog-ext-525001261-jspb": '[1,null,null,null,"2525e3954d185b3c"]'}, 71 | False, 72 | ) 73 | G_2_0_EXP_ADVANCED = ( 74 | "gemini-2.0-exp-advanced", 75 | {"x-goog-ext-525001261-jspb": '[null,null,null,null,"b1e46a6037e6aa9f"]'}, 76 | True, 77 | ) 78 | G_2_5_EXP_ADVANCED = ( 79 | "gemini-2.5-exp-advanced", 80 | {"x-goog-ext-525001261-jspb": '[null,null,null,null,"203e6bb81620bcfe"]'}, 81 | True, 82 | ) 83 | 84 | def __init__(self, name, header, advanced_only): 85 | """ 86 | Initialize a Model enum member. 87 | 88 | Args: 89 | name (str): Model name. 90 | header (dict): Model-specific headers. 91 | advanced_only (bool): If True, model is for advanced users only. 92 | """ 93 | self.model_name = name 94 | self.model_header = header 95 | self.advanced_only = advanced_only 96 | 97 | @classmethod 98 | def from_name(cls, name: str): 99 | """ 100 | Get a Model enum member by its model name. 101 | 102 | Args: 103 | name (str): Name of the model. 104 | 105 | Returns: 106 | Model: Corresponding Model enum member. 107 | 108 | Raises: 109 | ValueError: If the model name is not found. 110 | """ 111 | for model in cls: 112 | if model.model_name == name: 113 | return model 114 | raise ValueError( 115 | f"Unknown model name: {name}. Available models: {', '.join([model.model_name for model in cls])}" 116 | ) 117 | -------------------------------------------------------------------------------- /gemini_client/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | from pathlib import Path 4 | from typing import Dict, Tuple, Union, Optional 5 | 6 | from curl_cffi import CurlError 7 | from curl_cffi.requests import AsyncSession 8 | from requests.exceptions import RequestException, HTTPError, Timeout # Added Timeout 9 | 10 | from rich.console import Console 11 | 12 | # Assuming Endpoint and Headers enums are in 'enums.py' within the same package 13 | from .enums import Endpoint, Headers 14 | 15 | console = Console() # Instantiate console for logging 16 | 17 | async def upload_file( 18 | file: Union[bytes, str, Path], 19 | proxy: Optional[Union[str, Dict[str, str]]] = None, 20 | impersonate: str = "chrome110" 21 | ) -> str: 22 | """ 23 | Uploads a file to Google's Gemini server using curl_cffi and returns its identifier. 24 | 25 | Args: 26 | file (bytes | str | Path): File data in bytes or path to the file to be uploaded. 27 | proxy (str | dict, optional): Proxy URL or dictionary for the request. 28 | impersonate (str, optional): Browser profile for curl_cffi to impersonate. Defaults to "chrome110". 29 | 30 | Returns: 31 | str: Identifier of the uploaded file. 32 | 33 | Raises: 34 | HTTPError: If the upload request fails. 35 | RequestException: For other network-related errors. 36 | FileNotFoundError: If the file path does not exist. 37 | """ 38 | # Handle file input 39 | if not isinstance(file, bytes): 40 | file_path = Path(file) 41 | if not file_path.is_file(): 42 | raise FileNotFoundError(f"File not found at path: {file}") 43 | with open(file_path, "rb") as f: 44 | file_content = f.read() 45 | else: 46 | file_content = file 47 | 48 | # Prepare proxy dictionary for curl_cffi 49 | proxies_dict = None 50 | if isinstance(proxy, str): 51 | proxies_dict = {"http": proxy, "https": proxy} # curl_cffi uses http/https keys 52 | elif isinstance(proxy, dict): 53 | proxies_dict = proxy # Assume it's already in the correct format 54 | 55 | try: 56 | # Use AsyncSession from curl_cffi 57 | async with AsyncSession( 58 | proxies=proxies_dict, 59 | impersonate=impersonate, 60 | headers=Headers.UPLOAD.value # Pass headers directly 61 | # follow_redirects is handled automatically by curl_cffi 62 | ) as client: 63 | response = await client.post( 64 | url=Endpoint.UPLOAD.value, # Use Endpoint enum 65 | files={"file": file_content}, 66 | ) 67 | response.raise_for_status() # Raises HTTPError for bad responses 68 | return response.text 69 | except HTTPError as e: 70 | console.log(f"[red]HTTP error during file upload: {e.response.status_code} {e}[/red]") 71 | raise # Re-raise HTTPError 72 | except (RequestException, CurlError) as e: # Catch CurlError as well 73 | console.log(f"[red]Network error during file upload: {e}[/red]") 74 | raise # Re-raise other request errors 75 | 76 | def load_cookies(cookie_path: str) -> Tuple[str, str]: 77 | """ 78 | Loads authentication cookies from a JSON file. 79 | 80 | Args: 81 | cookie_path (str): Path to the JSON file containing cookies. 82 | 83 | Returns: 84 | tuple[str, str]: Tuple containing __Secure-1PSID and __Secure-1PSIDTS cookie values. 85 | 86 | Raises: 87 | Exception: If the file is not found, invalid, or required cookies are missing. 88 | """ 89 | try: 90 | with open(cookie_path, 'r', encoding='utf-8') as file: # Added encoding 91 | cookies = json.load(file) 92 | # Handle potential variations in cookie names (case-insensitivity) 93 | session_auth1 = next((item['value'] for item in cookies if item['name'].upper() == '__SECURE-1PSID'), None) 94 | session_auth2 = next((item['value'] for item in cookies if item['name'].upper() == '__SECURE-1PSIDTS'), None) 95 | 96 | if not session_auth1 or not session_auth2: 97 | raise StopIteration("Required cookies (__Secure-1PSID or __Secure-1PSIDTS) not found.") 98 | 99 | return session_auth1, session_auth2 100 | except FileNotFoundError: 101 | raise Exception(f"Cookie file not found at path: {cookie_path}") 102 | except json.JSONDecodeError: 103 | raise Exception("Invalid JSON format in the cookie file.") 104 | except StopIteration as e: 105 | raise Exception(f"{e} Check the cookie file format and content.") 106 | except Exception as e: # Catch other potential errors 107 | raise Exception(f"An unexpected error occurred while loading cookies: {e}") 108 | -------------------------------------------------------------------------------- /gemini.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const fs = require('fs').promises; 3 | const { v4: uuidv4 } = require('uuid'); 4 | 5 | class Gemini { 6 | constructor(cookiePath, timeout = 30000) { 7 | this.cookiePath = cookiePath; 8 | this.timeout = timeout; 9 | this.sessionAuth1 = ''; 10 | this.sessionAuth2 = ''; 11 | this.SNlM0e = ''; 12 | this.conversationId = ''; 13 | this.responseId = ''; 14 | this.choiceId = ''; 15 | } 16 | 17 | async initialize() { 18 | await this.loadCookies(); 19 | this.SNlM0e = await this.getSnlm0e(); 20 | } 21 | 22 | async loadCookies() { 23 | try { 24 | const cookieData = await fs.readFile(this.cookiePath, 'utf8'); 25 | const cookies = JSON.parse(cookieData); 26 | this.sessionAuth1 = cookies.find(item => item.name === '__Secure-1PSID')?.value; 27 | this.sessionAuth2 = cookies.find(item => item.name === '__Secure-1PSIDTS')?.value; 28 | 29 | if (!this.sessionAuth1 || !this.sessionAuth2) { 30 | throw new Error('Required cookies not found in the cookie file.'); 31 | } 32 | } catch (error) { 33 | throw new Error(`Failed to load cookies: ${error.message}`); 34 | } 35 | } 36 | 37 | async getSnlm0e() { 38 | try { 39 | const response = await axios.get('https://gemini.google.com/app', { 40 | timeout: 10000, 41 | headers: this.getHeaders(), 42 | }); 43 | 44 | const match = response.data.match(/"SNlM0e":"(.*?)"/); 45 | if (!match) { 46 | throw new Error('SNlM0e value not found in response.'); 47 | } 48 | 49 | return match[1]; 50 | } catch (error) { 51 | throw new Error(`Failed to retrieve SNlM0e: ${error.message}`); 52 | } 53 | } 54 | 55 | getHeaders() { 56 | return { 57 | 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', 58 | 'Host': 'gemini.google.com', 59 | 'Origin': 'https://gemini.google.com', 60 | 'Referer': 'https://gemini.google.com/', 61 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 62 | 'X-Same-Domain': '1', 63 | 'Cookie': `__Secure-1PSID=${this.sessionAuth1}; __Secure-1PSIDTS=${this.sessionAuth2}`, 64 | }; 65 | } 66 | 67 | async ask(question, sysPrompt = '') { 68 | try { 69 | const params = { 70 | bl: 'boq_assistant-bard-web-server_20230713.13_p0', 71 | _reqid: '0', 72 | rt: 'c', 73 | }; 74 | 75 | const messageStruct = [ 76 | [question], 77 | null, 78 | [this.conversationId, this.responseId, this.choiceId], 79 | ]; 80 | 81 | const data = new URLSearchParams({ 82 | 'f.req': JSON.stringify([null, JSON.stringify(messageStruct)]), 83 | 'at': this.SNlM0e, 84 | }); 85 | 86 | const response = await axios.post( 87 | 'https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate', 88 | data.toString(), 89 | { 90 | params, 91 | headers: this.getHeaders(), 92 | timeout: this.timeout, 93 | } 94 | ); 95 | 96 | const lines = response.data.split('\n'); 97 | const chatData = JSON.parse(lines[3])[0][2]; 98 | if (!chatData) { 99 | return { content: null, images: [] }; 100 | } 101 | 102 | const jsonChatData = JSON.parse(chatData); 103 | const images = []; 104 | 105 | if (jsonChatData[4]?.[0]?.[4]) { 106 | for (const imgData of jsonChatData[4][0][4]) { 107 | if (imgData?.[0]?.[0]?.[0]) { 108 | images.push(imgData[0][0][0]); 109 | } 110 | } 111 | } 112 | 113 | const results = { 114 | content: jsonChatData[4]?.[0]?.[1]?.[0] || null, 115 | conversationId: jsonChatData[1][0], 116 | responseId: jsonChatData[1][1], 117 | images, 118 | }; 119 | 120 | this.conversationId = results.conversationId; 121 | this.responseId = results.responseId; 122 | this.choiceId = jsonChatData[4]?.[0]?.[0] || ''; 123 | 124 | return results; 125 | } catch (error) { 126 | console.error(`An error occurred: ${error.message}`); 127 | return { content: null, images: [] }; 128 | } 129 | } 130 | } 131 | 132 | module.exports = Gemini; 133 | 134 | // Example usage: 135 | const readline = require('readline').createInterface({ 136 | input: process.stdin, 137 | output: process.stdout, 138 | }); 139 | 140 | async function main() { 141 | const gemini = new Gemini('cookie.json'); 142 | await gemini.initialize(); 143 | 144 | readline.question('>>> ', async (question) => { 145 | const response = await gemini.ask(question); 146 | console.log(response.content); 147 | readline.close(); 148 | }); 149 | } 150 | 151 | main().catch(console.error); 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gemini Client API 2 | 3 | This Python package provides a client for interacting with Google's Gemini API. It is built using `curl_cffi` for efficient and impersonated HTTP requests. 4 | 5 | ## Features 6 | 7 | - Asynchronous support using `asyncio`. 8 | - Synchronous wrapper for ease of use. 9 | - Conversation management (save, load). 10 | - File and image uploading. 11 | - Support for various Gemini models. 12 | - Image object handling (WebImage, GeneratedImage) with save functionality. 13 | - Proxy support. 14 | 15 | ## Installation 16 | 17 | 1. **Clone the repository:** 18 | ```bash 19 | git clone 20 | cd 21 | ``` 22 | 2. **Install dependencies:** 23 | It is recommended to use a virtual environment. 24 | ```bash 25 | python -m venv venv 26 | source venv/bin/activate # On Windows: venv\Scripts\activate 27 | pip install -r requirements.txt 28 | ``` 29 | (Ensure `requirements.txt` includes `curl_cffi`, `pydantic`, and `rich`.) 30 | 31 | Alternatively, if a `setup.py` is provided: 32 | ```bash 33 | pip install . 34 | ``` 35 | 36 | ## Usage 37 | 38 | ### Prerequisites 39 | 40 | You need to obtain your `__Secure-1PSID` and `__Secure-1PSIDTS` cookies from Google Gemini. 41 | 42 | 1. Go to [https://gemini.google.com/app](https://gemini.google.com/app) 43 | 2. Open your browser's developer tools (usually by pressing F12). 44 | 3. Go to the "Application" (or "Storage") tab. 45 | 4. Under "Cookies" -> "https://gemini.google.com", find the `__Secure-1PSID` and `__Secure-1PSIDTS` cookies. 46 | 5. Create a JSON file (e.g., `cookies.json`) with the following format: 47 | 48 | ```json 49 | [ 50 | { 51 | "name": "__Secure-1PSID", 52 | "value": "YOUR___SECURE-1PSID_VALUE_HERE" 53 | }, 54 | { 55 | "name": "__Secure-1PSIDTS", 56 | "value": "YOUR___SECURE-1PSIDTS_VALUE_HERE" 57 | } 58 | ] 59 | ``` 60 | 61 | ### Synchronous Chatbot 62 | 63 | ```python 64 | from gemini_client import Chatbot, Model 65 | 66 | # Initialize the chatbot 67 | try: 68 | chatbot = Chatbot(cookie_path="cookies.json", model=Model.G_2_5_PRO) 69 | except Exception as e: 70 | print(f"Error initializing chatbot: {e}") 71 | exit() 72 | 73 | # Ask a question 74 | try: 75 | response = chatbot.ask("Hello, how are you today?") 76 | if response and not response.get("error"): 77 | print("Gemini:", response["content"]) 78 | 79 | # Handling images in response 80 | if response.get("images"): 81 | print("\nImages found:") 82 | for img_data in response["images"]: 83 | # Note: The 'images' in the response are dicts. 84 | # To use the Image class features (like saving), you'd typically 85 | # instantiate Image objects from these dicts if needed, 86 | # especially if you want to use the save method directly on an Image object. 87 | # For generated images, you'd need to pass cookies to GeneratedImage. 88 | print(f"- Title: {img_data.get('title', '[Image]')}, URL: {img_data.get('url')}, Alt: {img_data.get('alt')}") 89 | else: 90 | print("Error or no content in response:", response) 91 | 92 | except Exception as e: 93 | print(f"Error during ask: {e}") 94 | 95 | # Ask a question with an image 96 | try: 97 | # Ensure 'image.png' exists or provide a valid path/bytes 98 | response_with_image = chatbot.ask("What is in this image?", image="path/to/your/image.png") 99 | if response_with_image and not response_with_image.get("error"): 100 | print("\nGemini (with image):", response_with_image["content"]) 101 | else: 102 | print("Error or no content in response with image:", response_with_image) 103 | except FileNotFoundError: 104 | print("Image file not found. Skipping ask with image example.") 105 | except Exception as e: 106 | print(f"Error during ask with image: {e}") 107 | 108 | # Save a conversation 109 | chatbot.save_conversation("conversations.json", "my_chat_session") 110 | print("\nConversation 'my_chat_session' saved.") 111 | 112 | # Load a conversation 113 | if chatbot.load_conversation("conversations.json", "my_chat_session"): 114 | print("Conversation 'my_chat_session' loaded.") 115 | # Continue chatting in the loaded conversation 116 | response_continued = chatbot.ask("What was our last topic?") 117 | if response_continued and not response_continued.get("error"): 118 | print("Gemini (continued):", response_continued["content"]) 119 | else: 120 | print("Error or no content in continued response:", response_continued) 121 | ``` 122 | 123 | ### Asynchronous Chatbot 124 | 125 | ```python 126 | import asyncio 127 | from gemini_client import AsyncChatbot, Model 128 | from gemini_client.utils import load_cookies # For loading cookies explicitly 129 | 130 | async def main(): 131 | try: 132 | secure_1psid, secure_1psidts = load_cookies("cookies.json") 133 | async_chatbot = await AsyncChatbot.create( 134 | secure_1psid=secure_1psid, 135 | secure_1psidts=secure_1psidts, 136 | model=Model.G_2_5_PRO 137 | ) 138 | except Exception as e: 139 | print(f"Error initializing async chatbot: {e}") 140 | return 141 | 142 | # Ask a question 143 | try: 144 | response = await async_chatbot.ask("Hello, asynchronously!") 145 | if response and not response.get("error"): 146 | print("Gemini (async):", response["content"]) 147 | else: 148 | print("Error or no content in async response:", response) 149 | except Exception as e: 150 | print(f"Error during async ask: {e}") 151 | 152 | # Example of saving an image if one was returned and properly parsed into an Image object 153 | # This part is illustrative, actual image saving depends on how you handle the response['images'] 154 | # if response and response.get("images"): 155 | # from gemini_client import WebImage # or GeneratedImage 156 | # first_image_data = response["images"][0] 157 | # # Assuming it's a web image for this example 158 | # img_obj = WebImage(url=first_image_data["url"], title=first_image_data.get("title"), alt=first_image_data.get("alt")) 159 | # # For GeneratedImage, you would need: 160 | # # img_obj = GeneratedImage(url=..., cookies=async_chatbot.session.cookies.get_dict()) 161 | # try: 162 | # saved_path = await img_obj.save(path="downloaded_async_images", verbose=True) 163 | # if saved_path: 164 | # print(f"Image saved to: {saved_path}") 165 | # except Exception as e: 166 | # print(f"Error saving image: {e}") 167 | 168 | # Close the session when done (important for AsyncSession) 169 | await async_chatbot.session.close() 170 | 171 | if __name__ == "__main__": 172 | # Example of how to run the async main function 173 | # In a real application, you might use asyncio.run(main()) 174 | # For simplicity in this README, we'll just call it if this script itself was run. 175 | # To run this example: 176 | # 1. Save this code as a Python file (e.g., example_async.py) 177 | # 2. Ensure cookies.json is present 178 | # 3. Run `python example_async.py` 179 | # 180 | # For this README, we'll just define it. 181 | # To run: 182 | # loop = asyncio.get_event_loop() 183 | # try: 184 | # loop.run_until_complete(main()) 185 | # except KeyboardInterrupt: 186 | # print("Exiting...") 187 | # finally: 188 | # # Clean up any pending tasks 189 | # for task in asyncio.all_tasks(loop): 190 | # task.cancel() 191 | # try: 192 | # loop.run_until_complete(loop.shutdown_asyncgens()) 193 | # finally: 194 | # loop.close() 195 | pass # Placeholder for running the async main if this were a runnable script 196 | ``` 197 | 198 | ## Modules 199 | 200 | The package is structured as follows: 201 | 202 | - `gemini_client/`: Main package directory. 203 | - `__init__.py`: Makes the directory a package and exports key components. 204 | - `core.py`: Contains `Chatbot` and `AsyncChatbot` classes. 205 | - `enums.py`: Defines `Endpoint`, `Headers`, and `Model` enums. 206 | - `images.py`: Defines `Image`, `WebImage`, and `GeneratedImage` classes for image handling. 207 | - `utils.py`: Contains utility functions like `upload_file` and `load_cookies`. 208 | 209 | ## Contributing 210 | 211 | Contributions are welcome! Please feel free to submit a pull request or open an issue. 212 | 213 | ## License 214 | 215 | This project is licensed under the MIT License. See the `LICENSE` file for details. 216 | -------------------------------------------------------------------------------- /gemini_client/images.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import random 4 | import re 5 | from datetime import datetime 6 | from pathlib import Path 7 | from typing import Dict, Union, Optional 8 | 9 | from pydantic import BaseModel, field_validator 10 | from curl_cffi import CurlError 11 | from curl_cffi.requests import AsyncSession 12 | from requests.exceptions import HTTPError, RequestException # Ensure RequestException is imported 13 | 14 | from rich.console import Console 15 | 16 | console = Console() # Instantiate console for logging 17 | 18 | class Image(BaseModel): 19 | """ 20 | Represents a single image object returned from Gemini. 21 | 22 | Attributes: 23 | url (str): URL of the image. 24 | title (str): Title of the image (default: "[Image]"). 25 | alt (str): Optional description of the image. 26 | proxy (str | dict | None): Proxy used when saving the image. 27 | impersonate (str): Browser profile for curl_cffi to impersonate. 28 | """ 29 | url: str 30 | title: str = "[Image]" 31 | alt: str = "" 32 | proxy: Optional[Union[str, Dict[str, str]]] = None 33 | impersonate: str = "chrome110" 34 | 35 | def __str__(self): 36 | return f"{self.title}({self.url}) - {self.alt}" 37 | 38 | def __repr__(self): 39 | short_url = self.url if len(self.url) <= 50 else self.url[:20] + "..." + self.url[-20:] 40 | short_alt = self.alt[:30] + "..." if len(self.alt) > 30 else self.alt 41 | return f"Image(title='{self.title}', url='{short_url}', alt='{short_alt}')" 42 | 43 | async def save( 44 | self, 45 | path: str = "downloaded_images", 46 | filename: Optional[str] = None, 47 | cookies: Optional[dict] = None, 48 | verbose: bool = False, 49 | skip_invalid_filename: bool = True, 50 | ) -> Optional[str]: 51 | """ 52 | Save the image to disk using curl_cffi. 53 | Parameters: 54 | path: str, optional 55 | Directory to save the image (default "downloaded_images"). 56 | filename: str, optional 57 | Filename to use; if not provided, inferred from URL. 58 | cookies: dict, optional 59 | Cookies used for the image request. 60 | verbose: bool, optional 61 | If True, outputs status messages (default False). 62 | skip_invalid_filename: bool, optional 63 | If True, skips saving if the filename is invalid. 64 | Returns: 65 | Absolute path of the saved image if successful; None if skipped. 66 | Raises: 67 | HTTPError if the network request fails. 68 | RequestException/CurlError for other network errors. 69 | IOError if file writing fails. 70 | """ 71 | # Generate filename from URL if not provided 72 | if not filename: 73 | try: 74 | from urllib.parse import urlparse, unquote 75 | parsed_url = urlparse(self.url) 76 | base_filename = os.path.basename(unquote(parsed_url.path)) 77 | # Remove invalid characters for filenames 78 | safe_filename = re.sub(r'[<>:"/\|?*]', '_', base_filename) 79 | if safe_filename and len(safe_filename) > 0: 80 | filename = safe_filename 81 | else: 82 | filename = f"image_{random.randint(1000, 9999)}.jpg" 83 | except Exception: 84 | filename = f"image_{random.randint(1000, 9999)}.jpg" 85 | 86 | # Validate filename length 87 | try: 88 | _ = Path(filename) 89 | max_len = 255 90 | if len(filename) > max_len: 91 | name, ext = os.path.splitext(filename) 92 | filename = name[:max_len - len(ext) - 1] + ext 93 | except (OSError, ValueError): 94 | if verbose: 95 | console.log(f"[yellow]Invalid filename generated: {filename}[/yellow]") 96 | if skip_invalid_filename: 97 | if verbose: 98 | console.log("[yellow]Skipping save due to invalid filename.[/yellow]") 99 | return None 100 | filename = f"image_{random.randint(1000, 9999)}.jpg" 101 | if verbose: 102 | console.log(f"[yellow]Using fallback filename: {filename}[/yellow]") 103 | 104 | # Prepare proxy dictionary for curl_cffi 105 | proxies_dict = None 106 | if isinstance(self.proxy, str): 107 | proxies_dict = {"http": self.proxy, "https": self.proxy} 108 | elif isinstance(self.proxy, dict): 109 | proxies_dict = self.proxy 110 | 111 | try: 112 | # Use AsyncSession from curl_cffi 113 | async with AsyncSession( 114 | cookies=cookies, 115 | proxies=proxies_dict, 116 | impersonate=self.impersonate 117 | # follow_redirects is handled automatically by curl_cffi 118 | ) as client: 119 | if verbose: 120 | console.log(f"Attempting to download image from: {self.url}") 121 | 122 | response = await client.get(self.url) 123 | response.raise_for_status() 124 | 125 | # Check content type 126 | content_type = response.headers.get("content-type", "").lower() 127 | if "image" not in content_type and verbose: 128 | console.log(f"[yellow]Warning: Content type is '{content_type}', not an image. Saving anyway.[/yellow]") 129 | 130 | # Create directory and save file 131 | dest_path = Path(path) 132 | dest_path.mkdir(parents=True, exist_ok=True) 133 | dest = dest_path / filename 134 | 135 | # Write image data to file 136 | dest.write_bytes(response.content) 137 | 138 | if verbose: 139 | console.log(f"Image saved successfully as {dest.resolve()}") 140 | 141 | return str(dest.resolve()) 142 | 143 | except HTTPError as e: 144 | console.log(f"[red]Error downloading image {self.url}: {e.response.status_code} {e}[/red]") 145 | raise 146 | except (RequestException, CurlError) as e: 147 | console.log(f"[red]Network error downloading image {self.url}: {e}[/red]") 148 | raise 149 | except IOError as e: 150 | console.log(f"[red]Error writing image file to {dest}: {e}[/red]") 151 | raise 152 | except Exception as e: 153 | console.log(f"[red]An unexpected error occurred during image save: {e}[/red]") 154 | raise 155 | 156 | 157 | class WebImage(Image): 158 | """ 159 | Represents an image retrieved from web search results. 160 | 161 | Returned when asking Gemini to "SEND an image of [something]". 162 | """ 163 | pass 164 | 165 | class GeneratedImage(Image): 166 | """ 167 | Represents an image generated by Google's AI image generator (e.g., ImageFX). 168 | 169 | Attributes: 170 | cookies (dict[str, str]): Cookies required for accessing the generated image URL, 171 | typically from the GeminiClient/Chatbot instance. 172 | """ 173 | cookies: Dict[str, str] 174 | 175 | # Updated validator for Pydantic V2 176 | @field_validator("cookies") 177 | @classmethod 178 | def validate_cookies(cls, v: Dict[str, str]) -> Dict[str, str]: 179 | """Ensures cookies are provided for generated images.""" 180 | if not v or not isinstance(v, dict): 181 | raise ValueError("GeneratedImage requires a dictionary of cookies from the client.") 182 | return v 183 | 184 | async def save(self, **kwargs) -> Optional[str]: 185 | """ 186 | Save the generated image to disk. 187 | Parameters: 188 | filename: str, optional 189 | Filename to use. If not provided, a default name including 190 | a timestamp and part of the URL is used. Generated images 191 | are often in .png or .jpg format. 192 | Additional arguments are passed to Image.save. 193 | Returns: 194 | Absolute path of the saved image if successful, None if skipped. 195 | """ 196 | if "filename" not in kwargs: 197 | ext = ".jpg" if ".jpg" in self.url.lower() else ".png" 198 | url_part = self.url.split('/')[-1][:10] 199 | kwargs["filename"] = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{url_part}{ext}" 200 | 201 | # Pass the required cookies and other args (like impersonate) to the parent save method 202 | return await super().save(cookies=self.cookies, **kwargs) 203 | -------------------------------------------------------------------------------- /gemini_client/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ######################################### 3 | # Code Modified to use curl_cffi 4 | ######################################### 5 | import asyncio 6 | import json 7 | import os 8 | import random 9 | import re 10 | import string 11 | from pathlib import Path 12 | from datetime import datetime 13 | from typing import Dict, List, Tuple, Union, Optional 14 | 15 | from gemini_client.enums import Endpoint, Headers, Model 16 | 17 | # Use curl_cffi for requests 18 | from curl_cffi import CurlError 19 | from curl_cffi.requests import AsyncSession 20 | # Import common request exceptions (curl_cffi often wraps these) 21 | from requests.exceptions import RequestException, Timeout, HTTPError 22 | 23 | # For image models using validation. Adjust based on organization internal pydantic. 24 | # Updated import for Pydantic V2 25 | from pydantic import BaseModel, field_validator 26 | 27 | # Rich is retained for logging within image methods. 28 | from rich.console import Console 29 | from rich.markdown import Markdown 30 | 31 | console = Console() 32 | 33 | ######################################### 34 | # New Enums and functions for endpoints, 35 | # headers, models, file upload and images. 36 | ######################################### 37 | 38 | ######################################### 39 | # Cookie loading and Chatbot classes 40 | ######################################### 41 | 42 | from gemini_client.utils import upload_file, load_cookies 43 | 44 | class Chatbot: 45 | """ 46 | Synchronous wrapper for the AsyncChatbot class. 47 | 48 | This class provides a synchronous interface to interact with Google Gemini, 49 | handling authentication, conversation management, and message sending. 50 | 51 | Attributes: 52 | loop (asyncio.AbstractEventLoop): Event loop for running async tasks. 53 | secure_1psid (str): Authentication cookie. 54 | secure_1psidts (str): Authentication cookie. 55 | async_chatbot (AsyncChatbot): Underlying asynchronous chatbot instance. 56 | """ 57 | def __init__( 58 | self, 59 | cookie_path: str, 60 | proxy: Optional[Union[str, Dict[str, str]]] = None, # Allow string or dict proxy 61 | timeout: int = 20, 62 | model: Model = Model.UNSPECIFIED, 63 | impersonate: str = "chrome110" # Added impersonate 64 | ): 65 | # Use asyncio.run() for cleaner async execution in sync context 66 | # Handle potential RuntimeError if an event loop is already running 67 | try: 68 | self.loop = asyncio.get_running_loop() 69 | except RuntimeError: 70 | self.loop = asyncio.new_event_loop() 71 | asyncio.set_event_loop(self.loop) 72 | 73 | self.secure_1psid, self.secure_1psidts = load_cookies(cookie_path) 74 | self.async_chatbot = self.loop.run_until_complete( 75 | AsyncChatbot.create(self.secure_1psid, self.secure_1psidts, proxy, timeout, model, impersonate) # Pass impersonate 76 | ) 77 | 78 | def save_conversation(self, file_path: str, conversation_name: str): 79 | return self.loop.run_until_complete( 80 | self.async_chatbot.save_conversation(file_path, conversation_name) 81 | ) 82 | 83 | def load_conversations(self, file_path: str) -> List[Dict]: 84 | return self.loop.run_until_complete( 85 | self.async_chatbot.load_conversations(file_path) 86 | ) 87 | 88 | def load_conversation(self, file_path: str, conversation_name: str) -> bool: 89 | return self.loop.run_until_complete( 90 | self.async_chatbot.load_conversation(file_path, conversation_name) 91 | ) 92 | 93 | def ask(self, message: str, image: Optional[Union[bytes, str, Path]] = None) -> dict: # Added image param 94 | # Pass image to async ask method 95 | return self.loop.run_until_complete(self.async_chatbot.ask(message, image=image)) 96 | 97 | class AsyncChatbot: 98 | """ 99 | Asynchronous chatbot client for interacting with Google Gemini using curl_cffi. 100 | 101 | This class manages authentication, session state, conversation history, 102 | and sending/receiving messages (including images) asynchronously. 103 | 104 | Attributes: 105 | headers (dict): HTTP headers for requests. 106 | _reqid (int): Request identifier for Gemini API. 107 | SNlM0e (str): Session token required for API requests. 108 | conversation_id (str): Current conversation ID. 109 | response_id (str): Current response ID. 110 | choice_id (str): Current choice ID. 111 | proxy (str | dict | None): Proxy configuration. 112 | proxies_dict (dict | None): Proxy dictionary for curl_cffi. 113 | secure_1psid (str): Authentication cookie. 114 | secure_1psidts (str): Authentication cookie. 115 | session (AsyncSession): curl_cffi session for HTTP requests. 116 | timeout (int): Request timeout in seconds. 117 | model (Model): Selected Gemini model. 118 | impersonate (str): Browser profile for curl_cffi to impersonate. 119 | """ 120 | __slots__ = [ 121 | "headers", 122 | "_reqid", 123 | "SNlM0e", 124 | "conversation_id", 125 | "response_id", 126 | "choice_id", 127 | "proxy", # Store the original proxy config 128 | "proxies_dict", # Store the curl_cffi-compatible proxy dict 129 | "secure_1psidts", 130 | "secure_1psid", 131 | "session", 132 | "timeout", 133 | "model", 134 | "impersonate", # Store impersonate setting 135 | ] 136 | 137 | def __init__( 138 | self, 139 | secure_1psid: str, 140 | secure_1psidts: str, 141 | proxy: Optional[Union[str, Dict[str, str]]] = None, # Allow string or dict proxy 142 | timeout: int = 20, 143 | model: Model = Model.UNSPECIFIED, 144 | impersonate: str = "chrome110", # Added impersonate 145 | ): 146 | headers = Headers.GEMINI.value.copy() 147 | if model != Model.UNSPECIFIED: 148 | headers.update(model.model_header) 149 | self._reqid = int("".join(random.choices(string.digits, k=7))) # Increased length for less collision chance 150 | self.proxy = proxy # Store original proxy setting 151 | self.impersonate = impersonate # Store impersonate setting 152 | 153 | # Prepare proxy dictionary for curl_cffi 154 | self.proxies_dict = None 155 | if isinstance(proxy, str): 156 | self.proxies_dict = {"http": proxy, "https": proxy} # curl_cffi uses http/https keys 157 | elif isinstance(proxy, dict): 158 | self.proxies_dict = proxy # Assume it's already in the correct format 159 | 160 | self.conversation_id = "" 161 | self.response_id = "" 162 | self.choice_id = "" 163 | self.secure_1psid = secure_1psid 164 | self.secure_1psidts = secure_1psidts 165 | 166 | # Initialize curl_cffi AsyncSession 167 | self.session = AsyncSession( 168 | headers=headers, 169 | cookies={"__Secure-1PSID": secure_1psid, "__Secure-1PSIDTS": secure_1psidts}, 170 | proxies=self.proxies_dict, 171 | timeout=timeout, 172 | impersonate=self.impersonate 173 | # verify and http2 are handled automatically by curl_cffi 174 | ) 175 | # No need to set proxies/headers/cookies again, done in constructor 176 | 177 | self.timeout = timeout # Store timeout for potential direct use in requests 178 | self.model = model 179 | self.SNlM0e = None # Initialize SNlM0e 180 | 181 | @classmethod 182 | async def create( 183 | cls, 184 | secure_1psid: str, 185 | secure_1psidts: str, 186 | proxy: Optional[Union[str, Dict[str, str]]] = None, # Allow string or dict proxy 187 | timeout: int = 20, 188 | model: Model = Model.UNSPECIFIED, 189 | impersonate: str = "chrome110", # Added impersonate 190 | ) -> "AsyncChatbot": 191 | """ 192 | Factory method to create and initialize an AsyncChatbot instance. 193 | Fetches the necessary SNlM0e value asynchronously. 194 | """ 195 | instance = cls(secure_1psid, secure_1psidts, proxy, timeout, model, impersonate) # Pass impersonate 196 | try: 197 | instance.SNlM0e = await instance.__get_snlm0e() 198 | except Exception as e: 199 | # Log the error and re-raise or handle appropriately 200 | console.log(f"[red]Error during AsyncChatbot initialization (__get_snlm0e): {e}[/red]", style="bold red") 201 | # Optionally close the session if initialization fails critically 202 | await instance.session.close() # Use close() for AsyncSession 203 | raise # Re-raise the exception to signal failure 204 | return instance 205 | 206 | async def save_conversation(self, file_path: str, conversation_name: str) -> None: 207 | # Logic remains the same 208 | conversations = await self.load_conversations(file_path) 209 | conversation_data = { 210 | "conversation_name": conversation_name, 211 | "_reqid": self._reqid, 212 | "conversation_id": self.conversation_id, 213 | "response_id": self.response_id, 214 | "choice_id": self.choice_id, 215 | "SNlM0e": self.SNlM0e, 216 | "model_name": self.model.model_name, # Save the model used 217 | "timestamp": datetime.now().isoformat(), # Add timestamp 218 | } 219 | 220 | found = False 221 | for i, conv in enumerate(conversations): 222 | if conv.get("conversation_name") == conversation_name: 223 | conversations[i] = conversation_data # Update existing 224 | found = True 225 | break 226 | if not found: 227 | conversations.append(conversation_data) # Add new 228 | 229 | try: 230 | # Ensure directory exists 231 | Path(file_path).parent.mkdir(parents=True, exist_ok=True) 232 | with open(file_path, "w", encoding="utf-8") as f: 233 | json.dump(conversations, f, indent=4, ensure_ascii=False) 234 | except IOError as e: 235 | console.log(f"[red]Error saving conversation to {file_path}: {e}[/red]") 236 | raise 237 | 238 | async def load_conversations(self, file_path: str) -> List[Dict]: 239 | # Logic remains the same 240 | if not os.path.isfile(file_path): 241 | return [] 242 | try: 243 | with open(file_path, 'r', encoding="utf-8") as f: 244 | return json.load(f) 245 | except (json.JSONDecodeError, IOError) as e: 246 | console.log(f"[red]Error loading conversations from {file_path}: {e}[/red]") 247 | return [] 248 | 249 | async def load_conversation(self, file_path: str, conversation_name: str) -> bool: 250 | # Logic remains the same, but update headers on the session 251 | conversations = await self.load_conversations(file_path) 252 | for conversation in conversations: 253 | if conversation.get("conversation_name") == conversation_name: 254 | try: 255 | self._reqid = conversation["_reqid"] 256 | self.conversation_id = conversation["conversation_id"] 257 | self.response_id = conversation["response_id"] 258 | self.choice_id = conversation["choice_id"] 259 | self.SNlM0e = conversation["SNlM0e"] 260 | if "model_name" in conversation: 261 | try: 262 | self.model = Model.from_name(conversation["model_name"]) 263 | # Update headers in the session if model changed 264 | self.session.headers.update(self.model.model_header) 265 | except ValueError as e: 266 | console.log(f"[yellow]Warning: Model '{conversation['model_name']}' from saved conversation not found. Using current model '{self.model.model_name}'. Error: {e}[/yellow]") 267 | 268 | console.log(f"Loaded conversation '{conversation_name}'") 269 | return True 270 | except KeyError as e: 271 | console.log(f"[red]Error loading conversation '{conversation_name}': Missing key {e}[/red]") 272 | return False 273 | console.log(f"[yellow]Conversation '{conversation_name}' not found in {file_path}[/yellow]") 274 | return False 275 | 276 | async def __get_snlm0e(self): 277 | """Fetches the SNlM0e value required for API requests using curl_cffi.""" 278 | if not self.secure_1psid: 279 | raise ValueError("__Secure-1PSID cookie is required.") 280 | 281 | try: 282 | # Use the session's get method 283 | resp = await self.session.get( 284 | Endpoint.INIT.value, 285 | timeout=self.timeout # Timeout is already set in session, but can override 286 | # follow_redirects is handled automatically by curl_cffi 287 | ) 288 | resp.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) 289 | 290 | # Check for authentication issues 291 | if "Sign in to continue" in resp.text or "accounts.google.com" in str(resp.url): 292 | raise PermissionError("Authentication failed. Cookies might be invalid or expired. Please update them.") 293 | 294 | # Regex to find the SNlM0e value 295 | snlm0e_match = re.search(r'["']SNlM0e["']\s*:\s*["'](.*?)["']', resp.text) 296 | if not snlm0e_match: 297 | error_message = "SNlM0e value not found in response." 298 | if resp.status_code == 429: 299 | error_message += " Rate limit likely exceeded." 300 | else: 301 | error_message += f" Response status: {resp.status_code}. Check cookie validity and network." 302 | raise ValueError(error_message) 303 | 304 | # Try to refresh PSIDTS if needed 305 | if not self.secure_1psidts and "PSIDTS" not in self.session.cookies: 306 | try: 307 | # Attempt to rotate cookies to get a fresh PSIDTS 308 | await self.__rotate_cookies() 309 | except Exception as e: 310 | console.log(f"[yellow]Warning: Could not refresh PSIDTS cookie: {e}[/yellow]") 311 | # Continue anyway as some accounts don't need PSIDTS 312 | 313 | return snlm0e_match.group(1) 314 | 315 | except Timeout as e: # Catch requests.exceptions.Timeout 316 | raise TimeoutError(f"Request timed out while fetching SNlM0e: {e}") from e 317 | except (RequestException, CurlError) as e: # Catch general request errors and Curl specific errors 318 | raise ConnectionError(f"Network error while fetching SNlM0e: {e}") from e 319 | except HTTPError as e: # Catch requests.exceptions.HTTPError 320 | if e.response.status_code == 401 or e.response.status_code == 403: 321 | raise PermissionError(f"Authentication failed (status {e.response.status_code}). Check cookies. {e}") from e 322 | else: 323 | raise Exception(f"HTTP error {e.response.status_code} while fetching SNlM0e: {e}") from e 324 | 325 | async def __rotate_cookies(self): 326 | """Rotates the __Secure-1PSIDTS cookie.""" 327 | try: 328 | response = await self.session.post( 329 | Endpoint.ROTATE_COOKIES.value, 330 | headers=Headers.ROTATE_COOKIES.value, 331 | data='[000,"-0000000000000000000"]', 332 | timeout=self.timeout 333 | ) 334 | response.raise_for_status() 335 | 336 | if new_1psidts := response.cookies.get("__Secure-1PSIDTS"): 337 | self.secure_1psidts = new_1psidts 338 | self.session.cookies.set("__Secure-1PSIDTS", new_1psidts) 339 | return new_1psidts 340 | except Exception as e: 341 | console.log(f"[yellow]Cookie rotation failed: {e}[/yellow]") 342 | raise 343 | 344 | 345 | async def ask(self, message: str, image: Optional[Union[bytes, str, Path]] = None) -> dict: 346 | """ 347 | Sends a message to Google Gemini and returns the response using curl_cffi. 348 | 349 | Parameters: 350 | message: str 351 | The message to send. 352 | image: Optional[Union[bytes, str, Path]] 353 | Optional image data (bytes) or path to an image file to include. 354 | 355 | Returns: 356 | dict: A dictionary containing the response content and metadata. 357 | """ 358 | if self.SNlM0e is None: 359 | raise RuntimeError("AsyncChatbot not properly initialized. Call AsyncChatbot.create()") 360 | 361 | params = { 362 | "bl": "boq_assistant-bard-web-server_20240625.13_p0", 363 | "_reqid": str(self._reqid), 364 | "rt": "c", 365 | } 366 | 367 | # Handle image upload if provided 368 | image_upload_id = None 369 | if image: 370 | try: 371 | # Pass proxy and impersonate settings to upload_file 372 | image_upload_id = await upload_file(image, proxy=self.proxies_dict, impersonate=self.impersonate) 373 | console.log(f"Image uploaded successfully. ID: {image_upload_id}") 374 | except Exception as e: 375 | console.log(f"[red]Error uploading image: {e}[/red]") 376 | return {"content": f"Error uploading image: {e}", "error": True} 377 | 378 | # Prepare message structure 379 | if image_upload_id: 380 | message_struct = [ 381 | [message], 382 | [[[image_upload_id, 1]]], 383 | [self.conversation_id, self.response_id, self.choice_id], 384 | ] 385 | else: 386 | message_struct = [ 387 | [message], 388 | None, 389 | [self.conversation_id, self.response_id, self.choice_id], 390 | ] 391 | 392 | # Prepare request data 393 | data = { 394 | "f.req": json.dumps([None, json.dumps(message_struct, ensure_ascii=False)], ensure_ascii=False), 395 | "at": self.SNlM0e, 396 | } 397 | 398 | try: 399 | # Send request 400 | resp = await self.session.post( 401 | Endpoint.GENERATE.value, 402 | params=params, 403 | data=data, 404 | timeout=self.timeout, 405 | ) 406 | resp.raise_for_status() 407 | 408 | # Process response 409 | lines = resp.text.splitlines() 410 | if len(lines) < 3: 411 | raise ValueError(f"Unexpected response format. Status: {resp.status_code}. Content: {resp.text[:200]}...") 412 | 413 | # Find the line with the response data 414 | chat_data_line = None 415 | for line in lines: 416 | if line.startswith(")]}'"): 417 | chat_data_line = line[4:].strip() 418 | break 419 | elif line.startswith("["): 420 | chat_data_line = line 421 | break 422 | 423 | if not chat_data_line: 424 | chat_data_line = lines[3] if len(lines) > 3 else lines[-1] 425 | if chat_data_line.startswith(")]}'"): 426 | chat_data_line = chat_data_line[4:].strip() 427 | 428 | # Parse the response JSON 429 | response_json = json.loads(chat_data_line) 430 | 431 | # Find the main response body 432 | body = None 433 | body_index = 0 434 | 435 | for part_index, part in enumerate(response_json): 436 | try: 437 | if isinstance(part, list) and len(part) > 2: 438 | main_part = json.loads(part[2]) 439 | if main_part and len(main_part) > 4 and main_part[4]: 440 | body = main_part 441 | body_index = part_index 442 | break 443 | except (IndexError, TypeError, json.JSONDecodeError): 444 | continue 445 | 446 | if not body: 447 | return {"content": "Failed to parse response body. No valid data found.", "error": True} 448 | 449 | # Extract data from the response 450 | try: 451 | # Extract main content 452 | content = "" 453 | if len(body) > 4 and len(body[4]) > 0 and len(body[4][0]) > 1: 454 | content = body[4][0][1][0] if len(body[4][0][1]) > 0 else "" 455 | 456 | # Extract conversation metadata 457 | conversation_id = body[1][0] if len(body) > 1 and len(body[1]) > 0 else self.conversation_id 458 | response_id = body[1][1] if len(body) > 1 and len(body[1]) > 1 else self.response_id 459 | 460 | # Extract additional data 461 | factualityQueries = body[3] if len(body) > 3 else None 462 | textQuery = body[2][0] if len(body) > 2 and body[2] else "" 463 | 464 | # Extract choices 465 | choices = [] 466 | if len(body) > 4: 467 | for candidate in body[4]: 468 | if len(candidate) > 1 and isinstance(candidate[1], list) and len(candidate[1]) > 0: 469 | choices.append({"id": candidate[0], "content": candidate[1][0]}) 470 | 471 | choice_id = choices[0]["id"] if choices else self.choice_id 472 | 473 | # Extract images - multiple possible formats 474 | images = [] 475 | 476 | # Format 1: Regular web images 477 | if len(body) > 4 and len(body[4]) > 0 and len(body[4][0]) > 4 and body[4][0][4]: 478 | for img_data in body[4][0][4]: 479 | try: 480 | img_url = img_data[0][0][0] 481 | img_alt = img_data[2] if len(img_data) > 2 else "" 482 | img_title = img_data[1] if len(img_data) > 1 else "[Image]" 483 | images.append({"url": img_url, "alt": img_alt, "title": img_title}) 484 | except (IndexError, TypeError): 485 | console.log("[yellow]Warning: Could not parse image data structure (format 1).[/yellow]") 486 | continue 487 | 488 | # Format 2: Generated images in standard location 489 | generated_images = [] 490 | if len(body) > 4 and len(body[4]) > 0 and len(body[4][0]) > 12 and body[4][0][12]: 491 | try: 492 | # Path 1: Check for images in [12][7][0] 493 | if body[4][0][12][7] and body[4][0][12][7][0]: 494 | # This is the standard path for generated images 495 | for img_index, img_data in enumerate(body[4][0][12][7][0]): 496 | try: 497 | img_url = img_data[0][3][3] 498 | img_title = f"[Generated Image {img_index+1}]" 499 | img_alt = img_data[3][5][0] if len(img_data[3]) > 5 and len(img_data[3][5]) > 0 else "" 500 | generated_images.append({"url": img_url, "alt": img_alt, "title": img_title}) 501 | except (IndexError, TypeError): 502 | continue 503 | 504 | # If we found images, but they might be in a different part of the response 505 | if not generated_images: 506 | # Look for image generation data in other response parts 507 | for part_index, part in enumerate(response_json): 508 | if part_index <= body_index: 509 | continue 510 | try: 511 | img_part = json.loads(part[2]) 512 | if img_part[4][0][12][7][0]: 513 | for img_index, img_data in enumerate(img_part[4][0][12][7][0]): 514 | try: 515 | img_url = img_data[0][3][3] 516 | img_title = f"[Generated Image {img_index+1}]" 517 | img_alt = img_data[3][5][0] if len(img_data[3]) > 5 and len(img_data[3][5]) > 0 else "" 518 | generated_images.append({"url": img_url, "alt": img_alt, "title": img_title}) 519 | except (IndexError, TypeError): 520 | continue 521 | break 522 | except (IndexError, TypeError, json.JSONDecodeError): 523 | continue 524 | except (IndexError, TypeError): 525 | pass 526 | 527 | # Format 3: Alternative location for generated images 528 | if len(generated_images) == 0 and len(body) > 4 and len(body[4]) > 0: 529 | try: 530 | # Try to find images in candidate[4] structure 531 | candidate = body[4][0] 532 | if len(candidate) > 22 and candidate[22]: 533 | # Look for URLs in the candidate[22] field 534 | import re 535 | content = candidate[22][0] if isinstance(candidate[22], list) and len(candidate[22]) > 0 else str(candidate[22]) 536 | urls = re.findall(r'https?://[^\s]+', content) 537 | for i, url in enumerate(urls): 538 | # Clean up URL if it ends with punctuation 539 | if url[-1] in ['.', ',', ')', ']', '}', '"', "'"]: 540 | url = url[:-1] 541 | generated_images.append({ 542 | "url": url, 543 | "title": f"[Generated Image {i+1}]", 544 | "alt": "" 545 | }) 546 | except (IndexError, TypeError) as e: 547 | console.log(f"[yellow]Warning: Could not parse alternative image structure: {e}[/yellow]") 548 | 549 | # Format 4: Look for image URLs in the text content 550 | if len(images) == 0 and len(generated_images) == 0 and content: 551 | try: 552 | import re 553 | # Look for image URLs in the content - try multiple patterns 554 | 555 | # Pattern 1: Standard image URLs 556 | urls = re.findall(r'(https?://[^\s]+\.(jpg|jpeg|png|gif|webp))', content.lower()) 557 | 558 | # Pattern 2: Google image URLs (which might not have extensions) 559 | google_urls = re.findall(r'(https?://lh\d+\.googleusercontent\.com/[^\s]+)', content) 560 | 561 | # Pattern 3: General URLs that might be images 562 | general_urls = re.findall(r'(https?://[^\s]+)', content) 563 | 564 | # Combine all found URLs 565 | all_urls = [] 566 | if urls: 567 | all_urls.extend([url_tuple[0] for url_tuple in urls]) 568 | if google_urls: 569 | all_urls.extend(google_urls) 570 | 571 | # Add general URLs only if we didn't find any specific image URLs 572 | if not all_urls and general_urls: 573 | all_urls = general_urls 574 | 575 | # Process all found URLs 576 | if all_urls: 577 | for i, url in enumerate(all_urls): 578 | # Clean up URL if it ends with punctuation 579 | if url[-1] in ['.', ',', ')', ']', '}', '"', "'"]: 580 | url = url[:-1] 581 | images.append({ 582 | "url": url, 583 | "title": f"[Image in Content {i+1}]", 584 | "alt": "" 585 | }) 586 | console.log(f"[green]Found {len(all_urls)} potential image URLs in content.[/green]") 587 | except Exception as e: 588 | console.log(f"[yellow]Warning: Error extracting URLs from content: {e}[/yellow]") 589 | 590 | # Combine all images 591 | all_images = images + generated_images 592 | 593 | # Prepare results 594 | results = { 595 | "content": content, 596 | "conversation_id": conversation_id, 597 | "response_id": response_id, 598 | "factualityQueries": factualityQueries, 599 | "textQuery": textQuery, 600 | "choices": choices, 601 | "images": all_images, 602 | "error": False, 603 | } 604 | 605 | # Update state 606 | self.conversation_id = conversation_id 607 | self.response_id = response_id 608 | self.choice_id = choice_id 609 | self._reqid += random.randint(1000, 9000) 610 | 611 | return results 612 | 613 | except (IndexError, TypeError) as e: 614 | console.log(f"[red]Error extracting data from response: {e}[/red]") 615 | return {"content": f"Error extracting data from response: {e}", "error": True} 616 | 617 | except json.JSONDecodeError as e: 618 | console.log(f"[red]Error parsing JSON response: {e}[/red]") 619 | return {"content": f"Error parsing JSON response: {e}. Response: {resp.text[:200]}...", "error": True} 620 | except Timeout as e: 621 | console.log(f"[red]Request timed out: {e}[/red]") 622 | return {"content": f"Request timed out: {e}", "error": True} 623 | except (RequestException, CurlError) as e: 624 | console.log(f"[red]Network error: {e}[/red]") 625 | return {"content": f"Network error: {e}", "error": True} 626 | except HTTPError as e: 627 | console.log(f"[red]HTTP error {e.response.status_code}: {e}[/red]") 628 | return {"content": f"HTTP error {e.response.status_code}: {e}", "error": True} 629 | except Exception as e: 630 | console.log(f"[red]An unexpected error occurred during ask: {e}[/red]", style="bold red") 631 | return {"content": f"An unexpected error occurred: {e}", "error": True} 632 | 633 | 634 | ######################################### 635 | # Imports for refactored classes 636 | ######################################### 637 | --------------------------------------------------------------------------------