├── grok3api ├── __init__.py ├── logger.py ├── types │ ├── GrokResponse.py │ └── GeneratedImage.py ├── server.py ├── history.py ├── driver.py └── client.py ├── assets ├── ship.jpg └── gopher.jpg ├── .gitignore ├── tests ├── ManyPictures.py ├── openai_test.py ├── SimpleTgBot │ └── SimpleTgBot.py └── example.py ├── Dockerfile ├── LICENSE ├── pyproject.toml ├── docs ├── Ru │ ├── LinuxDoc.md │ ├── ChangeLog.md │ ├── OpenAI_Server.md │ ├── ClientDoc.md │ ├── HistoryDoc.md │ ├── RuReadMe.md │ ├── askDoc.md │ └── GrokResponse.md └── En │ ├── LinuxDoc.md │ ├── ChangeLog.md │ ├── OpenAI_Server.md │ ├── ClientDoc.md │ ├── HistoryDoc.md │ ├── askDoc.md │ └── GrokResponse.md └── README.md /grok3api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/ship.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boykopovar/Grok3API/HEAD/assets/ship.jpg -------------------------------------------------------------------------------- /assets/gopher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boykopovar/Grok3API/HEAD/assets/gopher.jpg -------------------------------------------------------------------------------- /grok3api/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | # log_level = logging.DEBUG 4 | logger = logging.getLogger("grok3api") 5 | # logger.setLevel(log_level) 6 | 7 | # console_handler = logging.StreamHandler() 8 | # console_handler.setLevel(log_level) 9 | 10 | # formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s") 11 | # console_handler.setFormatter(formatter) 12 | # 13 | # logger.addHandler(console_handler) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyo 3 | *.pyd 4 | *.egg 5 | *.egg-info/ 6 | dist/ 7 | build/ 8 | 9 | .venv/ 10 | env/ 11 | venv/ 12 | ENV/ 13 | 14 | 15 | .idea/ 16 | .vscode/ 17 | 18 | *.dll 19 | *.so 20 | *.dylib 21 | *.exe 22 | 23 | .pytest_cache/ 24 | Grok3API.egg-info/ 25 | coverage.xml 26 | .coverage 27 | htmlcov/ 28 | images/ 29 | 30 | *.tar.gz 31 | *.zip 32 | *.whl 33 | 34 | *.log 35 | *.bak 36 | *.bat 37 | *.jpg 38 | *.png 39 | *.json 40 | 41 | !/assets/*.jpg 42 | !/assets/*.png 43 | 44 | .DS_Store 45 | Thumbs.db 46 | cookies.txt 47 | requirements.txt 48 | 49 | .env 50 | .env.local 51 | 52 | 53 | -------------------------------------------------------------------------------- /tests/ManyPictures.py: -------------------------------------------------------------------------------- 1 | import os 2 | from grok3api.client import GrokClient 3 | 4 | 5 | def main(): 6 | message = "Создай изображение корабля" 7 | client = GrokClient() 8 | os.makedirs("images", exist_ok=True) 9 | 10 | for i in range(5): 11 | result = client.ask(message) 12 | 13 | if result and result.modelResponse and result.modelResponse.generatedImages: 14 | image = result.modelResponse.generatedImages[0] 15 | image.save_to(f"images/{i}.png") 16 | else: 17 | print(f"Ошибка: не удалось получить изображение для {i}-й итерации.") 18 | 19 | if __name__ == '__main__': 20 | main() -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update && apt-get install -y \ 6 | wget \ 7 | xvfb \ 8 | libxi6 \ 9 | libgconf-2-4 \ 10 | libnss3 \ 11 | libx11-xcb1 \ 12 | libxcb1 \ 13 | libxcomposite1 \ 14 | libxcursor1 \ 15 | libxdamage1 \ 16 | libxfixes3 \ 17 | libxrandr2 \ 18 | libxrender1 \ 19 | libasound2 \ 20 | libatk1.0-0 \ 21 | libatk-bridge2.0-0 \ 22 | libgtk-3-0 \ 23 | ca-certificates \ 24 | fonts-liberation \ 25 | curl \ 26 | libgbm1 \ 27 | libvulkan1 \ 28 | xdg-utils \ 29 | && rm -rf /var/lib/apt/lists/* 30 | 31 | RUN wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \ 32 | && apt-get update && apt-get install -y ./google-chrome-stable_current_amd64.deb \ 33 | && rm -f google-chrome-stable_current_amd64.deb 34 | 35 | RUN pip install --no-cache-dir grok3api 36 | 37 | CMD ["python", "-m", "grok3api.server"] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 boykopovar 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. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "Grok3API" 7 | version = "0.1.0rc2" 8 | description = "Python-библиотека для взаимодействия с Grok3, ориентированная на максимальную простоту использования. Автоматически получает cookies, поэтому ничего не нужно указывать. A Python library for interacting with Grok3, focused on maximum ease of use. Automatically gets cookies, so you don't have to specify anything." 9 | authors = [{ name = "boykopovar", email = "boykopovar@gmail.com" }] 10 | license = "MIT" 11 | readme = "README.md" 12 | requires-python = ">=3.8" 13 | dependencies = ["undetected-chromedriver>=3.5.5"] 14 | keywords = ["grok3api", "grok 3 api", "grok api", "grok3api python", "grok ai", "unofficial grok3api api"] 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "Operating System :: OS Independent", 18 | ] 19 | 20 | [project.urls] 21 | Homepage = "https://github.com/boykopovar/Grok3API" 22 | 23 | [tool.setuptools] 24 | include-package-data = true 25 | 26 | [tool.setuptools.packages.find] 27 | where = ["."] 28 | include = ["grok3api*"] -------------------------------------------------------------------------------- /tests/openai_test.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | from openai import OpenAIError 3 | 4 | def send_message( message): 5 | """Отправляет сообщение на сервер через OpenAI клиент.""" 6 | client = OpenAI( 7 | base_url="http://localhost:9000/v1", 8 | api_key="dummy" 9 | ) 10 | try: 11 | response = client.chat.completions.create( 12 | model="grok-3", 13 | messages=[{"role": "user", "content": message}] 14 | ) 15 | print("Ответ сервера:") 16 | print(f"Модель: {response.model}") 17 | print(f"Сообщение: {response.choices[0].message.content}") 18 | print(f"Причина завершения: {response.choices[0].finish_reason}") 19 | print(f"Использование: {response.usage}") 20 | except OpenAIError as e: 21 | print(f"Ошибка: {e}") 22 | except Exception as e: 23 | print(f"Неожиданная ошибка: {e}") 24 | 25 | def main(): 26 | """Запрашивает куки и сообщение у пользователя и отправляет запрос.""" 27 | print("Введите сообщение:") 28 | message = input().strip() 29 | if not message: 30 | print("Сообщение не может быть пустым.") 31 | return 32 | send_message(message) 33 | 34 | if __name__ == "__main__": 35 | main() -------------------------------------------------------------------------------- /tests/SimpleTgBot/SimpleTgBot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from aiogram import Bot, Dispatcher, types 4 | from aiogram import Router, F 5 | from aiogram.filters import Command 6 | from aiogram.types import Message 7 | from grok3api.client import GrokClient 8 | 9 | logging.basicConfig( 10 | level=logging.INFO, 11 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" 12 | ) 13 | logger = logging.getLogger("aiogram") 14 | 15 | BOT_TOKEN = "BOT_TOKEN" 16 | 17 | bot = Bot(token=BOT_TOKEN) 18 | router = Router() 19 | dp = Dispatcher() 20 | 21 | @router.message(~F.text.startswith("/")) 22 | async def handle_message(message: Message): 23 | """Обрабатывает текст.""" 24 | if not message.text: 25 | await message.answer("Пустое сообщение.") 26 | return 27 | 28 | try: 29 | response = await GROK_CLIENT.async_ask(message=message.text, history_id=str(message.chat.id)) 30 | text_response = response.modelResponse.message if response.modelResponse else "" 31 | await message.answer(text_response) 32 | except Exception as e: 33 | print(f"Ошибка: {e}") 34 | await message.answer("Ошибка.") 35 | 36 | @router.message(Command("clear")) 37 | async def handle_clear_command(message: Message): 38 | """Очищает историю.""" 39 | try: 40 | GROK_CLIENT.history.del_history_by_id(str(message.chat.id)) 41 | await message.answer("История очищена.") 42 | except Exception as e: 43 | print(f"Ошибка: {e}") 44 | await message.answer("Ошибка очистки.") 45 | 46 | async def main(): 47 | """Запускает бота.""" 48 | dp.include_router(router) 49 | await dp.start_polling(bot) 50 | 51 | if __name__ == "__main__": 52 | GROK_CLIENT = GrokClient( 53 | history_msg_count=10, 54 | main_system_prompt="Отвечай только матом" 55 | ) 56 | asyncio.run(main()) -------------------------------------------------------------------------------- /tests/example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | import random 5 | 6 | from grok3api.client import GrokClient 7 | 8 | 9 | logging.basicConfig( 10 | level=logging.INFO, 11 | format="[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s" 12 | ) 13 | 14 | # urllib3_con_logger = logging.getLogger("urllib3.connectionpool") 15 | # for handler in urllib3_con_logger.handlers[:]: 16 | # urllib3_con_logger.removeHandler(handler) 17 | # urllib3_con_logger.setLevel(logging.DEBUG) 18 | 19 | # not necessary 20 | cookies_dict = { 21 | "i18nextLng": "ru", 22 | "sso-rw": "sso-rw-token-placeholder", 23 | "sso": "sso-token-placeholder", 24 | "cf_clearance": "cf_clearance-placeholder", 25 | "_ga": "ga-placeholder", 26 | "_ga_8FEWB057YH": "ga-8FEWB057YH-placeholder" 27 | } 28 | 29 | # not necessary 30 | cookie_str = ( 31 | "i18nextLng=ru; sso-rw=sso-rw-token-placeholder; sso=sso-token-placeholder; cf_clearance=cf_clearance-placeholder; _ga=ga-placeholder; _ga_8FEWB057YH=ga-8FEWB057YH-placeholder" 32 | ) 33 | 34 | async def main(): 35 | client = GrokClient( 36 | history_msg_count=0, # You can add cookies as str or dict (or List[dict or str]) format 37 | ) 38 | client.history.set_main_system_prompt("Отвечай коротко и с эмодзи") 39 | os.makedirs("images", exist_ok=True) 40 | while True: 41 | #prompt = input("Ведите запрос: ") 42 | prompt = str(random.randint(1, 100)) 43 | if prompt == "q": break 44 | result = await client.async_ask(message=prompt, 45 | modelName="grok-3", 46 | history_id="0", 47 | # images=["C:\\Users\\user\\Downloads\\photo.jpg", 48 | # "C:\\Users\\user\\Downloads\\скрин_сайта.png"], 49 | ) 50 | if result.error: 51 | print(f"Произошла ошибка: {result.error}") 52 | continue 53 | print(result.modelResponse.message) 54 | if result.modelResponse.generatedImages: 55 | for index, image in enumerate(result.modelResponse.generatedImages, start=1): 56 | image.save_to(f"images/gen_img_{index}.jpg") 57 | await client.history.async_to_file() 58 | 59 | if __name__ == '__main__': 60 | asyncio.run(main()) -------------------------------------------------------------------------------- /docs/Ru/LinuxDoc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 🐧 **Особенности работы с Linux** 4 | 5 | 6 | ### 🌟 **Основные требования** 7 | 8 | Для работы `GrokClient` в **Linux** необходимо наличие установленного **Google Chrome**. Если используется серверная версия Linux (например, **Ubuntu Server**) без графического интерфейса, потребуется установить **Xvfb** для эмуляции виртуального экрана, что обеспечит работу Chrome в headless режиме. ✨ 9 | 10 | --- 11 | 12 | ### 🛠️ **Установка Google Chrome на Linux** 13 | 14 | Для установки **Google Chrome** откройте терминал и выполните следующую команду (пример): 15 | 16 | ```bash 17 | wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - 18 | echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list 19 | ``` 20 | 21 | ```bash 22 | sudo apt update 23 | sudo apt install -y google-chrome-stable 24 | ``` 25 | 26 | --- 27 | 28 | ### 🎥 **Установка Xvfb для headless режима** 29 | 30 | На системах без графического интерфейса установка **Xvfb** позволяет создать виртуальный экран. Для установки выполните: 31 | 32 | ```bash 33 | sudo apt update 34 | sudo apt install -y xvfb 35 | ``` 36 | 37 | > 🌟 **Примечание:** Xvfb создаёт виртуальный экран с минимальными характеристиками, что позволяет запускать Chrome без необходимости наличия физического дисплея. 38 | 39 | --- 40 | 41 | ### ⚙️ **Параметры использования Xvfb** 42 | 43 | При создании экземпляра `GrokClient` доступны следующие параметры для настройки работы с Xvfb: 44 | 45 | | Параметр | Тип | Описание | Значение по умолчанию | 46 | |-------------------|--------|-------------------------------------------------------------------------------|-----------------------| 47 | | `use_xvfb` | `bool` | Флаг, определяющий использование Xvfb в Linux. | `True` | 48 | 49 | > ❗ **Важно:** На Linux по умолчанию используется `use_xvfb=True`. При наличии графического интерфейса данную опцию рекомендуется отключить. 50 | 51 | --- 52 | 53 | ### 🌟 **Пример: Отключение Xvfb при наличии графического интерфейса** 54 | 55 | При наличии графического интерфейса можно создать экземпляр клиента следующим образом: 56 | 57 | ```python 58 | from grok3api.client import GrokClient 59 | 60 | client = GrokClient(use_xvfb=False) 61 | ``` 62 | 63 | > 💡 В данном случае приложение будет использовать реальный графический интерфейс. 64 | 65 | --- 66 | 67 | ### 📌 **Итог** 68 | 69 | - **Xvfb** используется для эмуляции графического экрана на системах без GUI. 70 | - По умолчанию `use_xvfb=True`; при наличии графического интерфейса данную опцию следует отключить. 71 | -------------------------------------------------------------------------------- /docs/En/LinuxDoc.md: -------------------------------------------------------------------------------- 1 | ## 🐧 **Specifics of Working with Linux** 2 | 3 | ### 🌟 **Basic Requirements** 4 | 5 | For `GrokClient` to work on **Linux**, it is necessary to have **Google Chrome** installed. If you are using a server version of Linux (e.g., **Ubuntu Server**) without a graphical interface, you will need to install **Xvfb** to emulate a virtual display, which will ensure that Chrome operates in headless mode. ✨ 6 | 7 | --- 8 | 9 | ### 🛠️ **Installing Google Chrome on Linux** 10 | 11 | To install **Google Chrome**, open a terminal and execute the following command (example): 12 | 13 | ```bash 14 | wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - 15 | echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list 16 | ``` 17 | 18 | ```bash 19 | sudo apt update 20 | sudo apt install -y google-chrome-stable 21 | ``` 22 | 23 | --- 24 | 25 | ### 🎥 **Installing Xvfb for Headless Mode** 26 | 27 | On systems without a graphical interface, installing **Xvfb** allows you to create a virtual display. To install, execute: 28 | 29 | ```bash 30 | sudo apt update 31 | sudo apt install -y xvfb 32 | ``` 33 | 34 | > 🌟 **Note:** Xvfb creates a virtual display with minimal specifications, enabling Chrome to run without the need for a physical display. 35 | 36 | --- 37 | 38 | ### ⚙️ **Parameters for Using Xvfb** 39 | 40 | When creating an instance of `GrokClient`, the following parameters are available to configure the use of Xvfb: 41 | 42 | | Parameter | Type | Description | Default Value | 43 | |-------------------|--------|--------------------------------------------------------------------------------------------------------------|---------------| 44 | | `use_xvfb` | `bool` | A flag determining the use of Xvfb on Linux. | `True` | 45 | 46 | > ❗ **Important:** On Linux, `use_xvfb=True` is used by default. If a graphical interface is present, it is recommended to disable this option. 47 | 48 | --- 49 | 50 | ### 🌟 **Example: Disabling Xvfb When a Graphical Interface is Present** 51 | 52 | If a graphical interface is available, you can create an instance of the client as follows: 53 | 54 | ```python 55 | from grok3api.client import GrokClient 56 | 57 | client = GrokClient(use_xvfb=False) 58 | ``` 59 | 60 | > 💡 In this case, the application will use the real graphical interface. 61 | 62 | --- 63 | 64 | ### 📌 **Summary** 65 | 66 | - **Xvfb** is used to emulate a graphical display on systems without a GUI. 67 | - By default, `use_xvfb=True`; if a graphical interface is present, this option should be disabled. 68 | -------------------------------------------------------------------------------- /docs/En/ChangeLog.md: -------------------------------------------------------------------------------- 1 | ## 📦 Changelog 2 | 3 | 4 | --- 5 | 6 | ### 🆕 v0.1.0b1 7 | 8 | #### ✨ New: 9 | 10 | * 🛠 **Improved code block handling** 11 | Added automatic transformation of nested blocks `...` into standard Markdown code blocks with language indication. Also, support for converting non-standard code block headers: 12 | 13 | * ` ```x-src` → ` ```` 14 | * ` ```x-` → ` ```` 15 | 16 | This improves code display, regardless of the original format. 17 | 18 | * ☑️ The feature can be disabled by setting the `auto_transform_code_blocks=False` parameter when creating `GrokClient`. 19 | 20 | 21 | 22 | --- 23 | 24 | ### 🆕 v0.0.9b1 25 | 26 | #### ✨ New: 27 | - 💬 **Support for continuing existing conversations with Grok ([issue](https://github.com/boykopovar/Grok3API/issues/4))** 28 | `client.ask` can now automatically continue an existing conversation using `conversation_id` and `response_id`. These parameters are retrieved automatically after sending the first message but can also be manually set when creating the client. 29 | If valid, requests are sent to the current conversation instead of creating a new one, which improves history management and responsiveness. 30 | 31 | - ➕ **New parameters for conversation control** 32 | - `always_new_conversation` — client parameter: always starts a new chat regardless of previous messages. 33 | - `new_conversation` — `ask` method parameter: manually starts a new chat for the current request (does not affect the saved History). 34 | When using server-side history, the History from the class will only be added to the first message of each server-side chat (e.g., when cookies are rotated, a new server chat is created). 35 | 36 | - 🆙 **Extended `GrokResponse` object** 37 | Now also includes additional conversation-related fields: 38 | - `conversationId` — conversation identifier. 39 | - `title` — chat title if generated or updated. 40 | - `conversationCreateTime` — conversation creation timestamp. 41 | - `conversationModifyTime` — last conversation modification timestamp. 42 | - `temporary` — temporary chat flag (`True` — temporary, `False` — permanent, `None` — unknown). 43 | 44 | --- 45 | 46 | #### 📋 Notes: 47 | - ✅ Chats are now saved on Grok servers and loaded automatically during requests even without using the built-in `History`. 48 | - ⚠️ Chats created with different cookies cannot be loaded — this is a server-side limitation (rotating cookies automatically creates a new server chat). 49 | 50 | #### 📚 More: 51 | - [💼️ GrokClient class description](ClientDoc.md) 52 | - [✈️ `ask` method description](askDoc.md) 53 | - [📋 History class description](HistoryDoc.md) 54 | - [📬 GrokResponse class description](GrokResponse.md) 55 | 56 | --- 57 | 58 | 59 | ### 🆕 v0.0.1b11 60 | 61 | #### ✨ New: 62 | - 🖼️ **Support for sending images to Grok** 63 | It's now much easier to send images to the Grok server! 64 | 65 | ```python 66 | from grok3api.client import GrokClient 67 | 68 | 69 | def main(): 70 | client = GrokClient() 71 | result = client.ask( 72 | message="What's in the picture?", 73 | images="C:\\photo1_to_grok.jpg" 74 | ) 75 | print(f"Grok3 Response: {result.modelResponse.message}") 76 | 77 | if __name__ == '__main__': 78 | main() 79 | ``` 80 | 81 | A detailed description of the method for sending images is available [here](askDoc.md). 82 | 83 | - 🤖 **Work continues on OpenAI-compatible server** 84 | - ✅ Now **any `api_key`** is supported for working with the server. 85 | - ⚙️ Added the ability to configure the server (via command-line arguments and environment variables). A detailed guide on how to run the server is available in [🌐 Running the OpenAI-Compatible Server](OpenAI_Server.md). 86 | 87 | > ⚠️ **The server is still in early stages, and some features may be unstable.** 88 | 89 | --- 90 | 91 | 92 | ### 🆕 v0.0.1b10 93 | 94 | #### ✨ New: 95 | - 🔐 **Automatic cookie retrieval** 96 | It is now **no longer required to manually provide cookies** — the client will automatically retrieve them if needed. 97 | ➕ However, you **can still provide your own cookies** if you want to use your account (e.g., for generation with custom settings or access to premium features). 98 | 99 | - 🤖 **Started work on OpenAI compatibility** ([**server.py**](../../grok3api/server.py), [**Demo**](../../tests/openai_test.py)) 100 | The initial draft of OpenAI API compatibility server has been implemented. It **kind of works**, but: 101 | > ⚠️ **It's in a very early stage and completely unpolished. Use at your own risk!** 102 | 103 | --- 104 | 105 | -------------------------------------------------------------------------------- /docs/Ru/ChangeLog.md: -------------------------------------------------------------------------------- 1 | ## 📦 Changelog 2 | 3 | 4 | --- 5 | 6 | ### 🆕 v0.1.0b1 7 | 8 | #### ✨ Новое: 9 | 10 | * 🛠 **Улучшена обработка код-блоков** 11 | Добавлена автоматическая трансформация вложенных блоков `...` в стандартные Markdown-блоки с указанием языка. Также поддерживается преобразование нестандартных заголовков код-блоков: 12 | 13 | * ` ```x-src` → ` ```` 14 | * ` ```x-` → ` ```` 15 | 16 | Это улучшает отображение кода, независимо от исходного формата. 17 | 18 | * ☑️ Функцию можно отключить, указав параметр `auto_transform_code_blocks=False` при создании `GrokClient`. 19 | 20 | --- 21 | 22 | 23 | ### 🆕 v0.0.9b1 24 | 25 | #### ✨ Новое: 26 | - 💬 **Поддержка продолжения существующих бесед с Grok (Решение [issue](https://github.com/boykopovar/Grok3API/issues/4))** 27 | Теперь `client.ask` может автоматически продолжать уже начатую беседу, используя `conversation_id` и `response_id`. Эти параметры получаются автоматически при отправке первого сообщения, но можно задать свои при создании клиента. 28 | Если они получены и валидны, запрос отправляется в текущую беседу, а не создаётся новый чат, что в какой-то мере автоматизирует управление историей и повышает отзывчивость. 29 | 30 | - ➕ **Добавлены новые параметры для управления беседами** 31 | - `always_new_conversation` — параметр клиента: позволяет всегда начинать новый чат независимо от предыдущих сообщений. 32 | - `new_conversation` — параметр метода `ask`: позволяет начать новый чат вручную при отправке запроса (не влияет на сохранённую в классе History историю). История из класса History при использовании истории с сервера будет добавлена только к первому сообщению каждого серверного чата (например, при авто-смене cookies создаётся новый серверный чат). 33 | 34 | - 🆙 **Расширен объект `GrokResponse`** 35 | Теперь дополнительно возвращаются данные о беседе (ниже только новые поля): 36 | - `conversationId` — идентификатор чата. 37 | - `title` — заголовок чата, если был сгенерирован или обновлён. 38 | - `conversationCreateTime` — время создания чата. 39 | - `conversationModifyTime` — время последнего изменения чата. 40 | - `temporary` — флаг временного чата (`True` — временный, `False` — постоянный, `None` — неизвестно). 41 | 42 | 43 | 44 | #### 📋 Примечания: 45 | - ✅ Теперь даже без использования встроенной истории (`History`) чаты сохраняются на серверах Grok и подгружаются при запросах. 46 | - ⚠️ Чаты, созданные с другими cookies, не могут быть загружены — это ограничение серверной стороны (при автоматической смене cookies создаётся новый серверный чат). 47 | 48 | #### 📚 Подробнее 49 | - [💼️ Описание класса `GrokClient`](ClientDoc.md) 50 | - [✈️ Описание метода `ask`](askDoc.md) 51 | - [📋 Описание класса `History`](HistoryDoc.md) 52 | - [📬 Описание класса `GrokResponse`](GrokResponse.md) 53 | 54 | --- 55 | 56 | 57 | 58 | ### 🆕 v0.0.1b11 59 | 60 | #### ✨ Новое: 61 | - 🖼️ **Поддержка отправки изображений в Grok** 62 | Теперь стало намного проще отправлять изображения на сервер Grok! 63 | 64 | ```python 65 | from grok3api.client import GrokClient 66 | 67 | 68 | def main(): 69 | client = GrokClient() 70 | result = client.ask( 71 | message="Что на картинке?", 72 | images="C:\\photo1_to_grok.jpg" 73 | ) 74 | print(f"Ответ Grok3: {result.modelResponse.message}") 75 | 76 | if __name__ == '__main__': 77 | main() 78 | ``` 79 | 80 | Подробное описание метода отправки доступно [здесь](askDoc.md). 81 | 82 | - 🤖 **Продолжается работа над OpenAI-совместимостью** 83 | - ✅ Теперь поддерживаются **любые `api_key`** для работы с сервером. 84 | - ⚙️ Добавлена возможность настройки сервера (через аргументы командной строки и переменные окружения). Подробная инструкция по запуску доступна в [🌐 Запуск OpenAI-совместимого сервера](OpenAI_Server.md). 85 | 86 | > ⚠️ **Сервер всё ещё находится на ранней стадии, некоторые функции могут быть нестабильными.** 87 | 88 | --- 89 | 90 | ### 🆕 v0.0.1b10 91 | 92 | #### ✨ Новое: 93 | - 🔐 **Автоматическое получение cookies** 94 | Теперь больше **не обязательно указывать cookies вручную** — клиент сам их получит при необходимости. 95 | ➕ Однако, вы **всё ещё можете указать свои cookies**, если хотите использовать свой аккаунт (например, для генерации с кастомными настройками или доступом к платным возможностям). 96 | 97 | - 🤖 **Начало работы над OpenAI-совместимостью ([**server.py**](../../grok3api/server.py), [**Demo**](../../tests/openai_test.py))** 98 | Реализован первый черновик сервера, совместимого с API OpenAI. Пока **работает "как-то"**, но: 99 | > ⚠️ **Находится на ранней стадии и не отлажен. Используйте на свой страх и риск!** 100 | 101 | --- -------------------------------------------------------------------------------- /docs/En/OpenAI_Server.md: -------------------------------------------------------------------------------- 1 | # 🧠 Grok3API OpenAI-Compatible Server 2 | 3 | > 🤖 **Early stage of server development** 4 | 5 | The first draft of a server compatible with the OpenAI API (`/v1/chat/completions`) has been implemented. 6 | 7 | > ⚠️ **It's in the early stage and not fully debugged. Use it at your own risk!** 8 | 9 | --- 10 | 11 | ### ⚙️ Running the Server 12 | 13 | > Make sure you have `fastapi`, `uvicorn`, `pydantic`, and `grok3api` installed. 14 | 15 | ```bash 16 | # From the project root: 17 | python -m grok3api.server 18 | ``` 19 | 20 | ```bash 21 | # Or you can configure the host and port 22 | python -m grok3api.server --host 127.0.0.1 --port 9000 23 | ``` 24 | 25 | 🎉 The server will start at `http://127.0.0.1:9000`. 26 | 27 | > Uses `uvicorn` under the hood. Make sure the project structure allows for module-based execution (via `-m`). 28 | 29 | --- 30 | 31 | ## 🔁 Endpoint: `/v1/chat/completions` 32 | 33 | Compatible with the OpenAI request format: 34 | 35 | ### 📥 Example request: 36 | 37 | ```python 38 | from openai import OpenAI 39 | from openai import OpenAIError 40 | 41 | # Creating a client with the specified URL and key 42 | client = OpenAI( 43 | base_url="http://localhost:9000/v1", 44 | api_key="dummy" 45 | ) 46 | 47 | try: 48 | # Sending a request to the server 49 | response = client.chat.completions.create( 50 | model="grok-3", 51 | messages=[{"role": "user", "content": "What is Python?"}] 52 | ) 53 | 54 | # Displaying the response from the server 55 | print("Server Response:") 56 | print(f"Model: {response.model}") 57 | print(f"Message: {response.choices[0].message.content}") 58 | print(f"Finish Reason: {response.choices[0].finish_reason}") 59 | print(f"Usage: {response.usage}") 60 | 61 | except OpenAIError as e: 62 | print(f"Error: {e}") 63 | except Exception as e: 64 | print(f"Unexpected error: {e}") 65 | ``` 66 | 67 | > `stream` is not supported, a `400 Bad Request` will be returned. 68 | 69 | --- 70 | 71 | ### 📤 Output: 72 | 73 | ``` 74 | Server Response: 75 | Model: grok-3 76 | Message: Python is a high-level, interpreted programming language with a simple and readable syntax. It supports multiple programming paradigms (object-oriented, functional, procedural) and is widely used for web development, data analysis, task automation, machine learning, and more. Python is popular for its versatility, extensive standard library, and large developer community. 77 | Finish Reason: stop 78 | Usage: CompletionUsage(completion_tokens=46, prompt_tokens=3, total_tokens=49, completion_tokens_details=None, prompt_tokens_details=None) 79 | ``` 80 | 81 | --- 82 | 83 | 84 | ## 🧵 Endpoint: `/v1/string` (GET) 85 | 86 | A simple text GET request. Accepts a string as a query parameter and returns a response from Grok without JSON wrapping. 87 | 88 | ### 📥 Example Request: 89 | 90 | Open in a browser or use `curl`: 91 | 92 | ```bash 93 | curl http://localhost:9000/v1/string?q=Hello 94 | ``` 95 | 96 | ### 📤 Example Response: 97 | 98 | ``` 99 | Hello! How can I help? 100 | ``` 101 | 102 | ### 🐍 Example in Python: 103 | 104 | ```python 105 | import requests 106 | 107 | response = requests.get("http://localhost:9000/v1/string", params={"q": "Tell a joke"}) 108 | print(response.text) # Just text, no JSON 109 | ``` 110 | 111 | --- 112 | 113 | ## 🧵 Endpoint: `/v1/string` (POST) 114 | 115 | A simple text POST request. Accepts a string in the request body and returns a response from Grok without JSON wrapping. 116 | 117 | ### 📥 Example Request: 118 | 119 | Use `curl` or another tool: 120 | 121 | ```bash 122 | curl -X POST http://localhost:9000/v1/string -d "Hello" 123 | ``` 124 | 125 | ### 📤 Example Response: 126 | 127 | ``` 128 | Hello! How can I help? 129 | ``` 130 | 131 | ### 🐍 Example in Python: 132 | 133 | ```python 134 | import requests 135 | 136 | response = requests.post("http://localhost:9000/v1/string", data="Tell a joke") 137 | print(response.text) # Just text, no JSON 138 | ``` 139 | 140 | --- 141 | 142 | 143 | 144 | ## ⚙️ Environment Variables (optional) 145 | 146 | | Variable | Description | Default Value | 147 | |--------------------|---------------------------------------------|---------------| 148 | | `GROK_COOKIES` | Cookies file for GrokClient | `None` | 149 | | `GROK_PROXY` | Proxy (e.g., `http://localhost:8080`) | `None` | 150 | | `GROK_TIMEOUT` | Grok request timeout (in seconds) | `120` | 151 | | `GROK_SERVER_HOST` | IP address for running the server | `0.0.0.0` | 152 | | `GROK_SERVER_PORT` | Port for running the server | `8000` | 153 | 154 | --- 155 | 156 | ## 📂 Project Structure 157 | 158 | ``` 159 | Grok3API/ 160 | ├── grok3api/ 161 | │ ├── server.py # <--- running the server 162 | │ ├── client.py 163 | │ ├── types/ 164 | │ └── ... 165 | ├── tests/ 166 | │ └── openai_test.py # <--- compatibility test 167 | └── README.md 168 | ``` 169 | 170 | --- 171 | 172 | ## ❗ TODO / Known Issues 173 | 174 | - [ ] 🔄 Streaming support (`stream=True`) 175 | - [ ] 🧪 More tests and validation 176 | - [ ] 🧼 Refactor `message_payload` and history logic 177 | - [ ] 🧩 Custom instructions, images, and additional features 178 | 179 | --- 180 | 181 | 💬 For questions and suggestions — feel free to open an issue! -------------------------------------------------------------------------------- /docs/Ru/OpenAI_Server.md: -------------------------------------------------------------------------------- 1 | # 🧠 Grok3API OpenAI-Compatible Server 2 | 3 | > 🤖 **Ранняя стадия разработки сервера** 4 | 5 | Реализован первый черновик сервера, совместимого с OpenAI API (`/v1/chat/completions`). 6 | 7 | > ⚠️ **Находится на ранней стадии и не отлажен. Используйте на свой страх и риск!** 8 | 9 | --- 10 | 11 | ### ⚙️ Запуск сервера 12 | 13 | > Убедитесь, что у вас установлен `fastapi`, `uvicorn`, `pydantic`, и сам `grok3api`. 14 | 15 | ```bash 16 | # В корне проекта: 17 | python -m grok3api.server 18 | ``` 19 | 20 | ```bash 21 | # Или можете настроить адрес и порт 22 | python -m grok3api.server --host 127.0.0.1 --port 9000 23 | ``` 24 | 25 | 🎉 Сервер запустится на `http://127.0.0.1:9000`. 26 | 27 | > Используется `uvicorn` под капотом. Убедитесь, что структура проекта позволяет модульный запуск (через `-m`). 28 | 29 | --- 30 | 31 | ## 🔁 Эндпоинт: `/v1/chat/completions` 32 | 33 | Совместим с OpenAI форматом запросов: 34 | 35 | ### 📥 Пример запроса: 36 | 37 | ```python 38 | from openai import OpenAI 39 | from openai import OpenAIError 40 | 41 | # Создание клиента с заданным URL и ключом 42 | client = OpenAI( 43 | base_url="http://localhost:9000/v1", 44 | api_key="dummy" 45 | ) 46 | 47 | try: 48 | # Отправка запроса на сервер 49 | response = client.chat.completions.create( 50 | model="grok-3", 51 | messages=[{"role": "user", "content": "Что такое Python?"}] 52 | ) 53 | 54 | # Вывод ответа от сервера 55 | print("Ответ сервера:") 56 | print(f"Модель: {response.model}") 57 | print(f"Сообщение: {response.choices[0].message.content}") 58 | print(f"Причина завершения: {response.choices[0].finish_reason}") 59 | print(f"Использование: {response.usage}") 60 | 61 | except OpenAIError as e: 62 | print(f"Ошибка: {e}") 63 | except Exception as e: 64 | print(f"Неожиданная ошибка: {e}") 65 | 66 | ``` 67 | 68 | > `stream` не поддерживается, будет возвращён `400 Bad Request`. 69 | 70 | 71 | ### 📤 Вывод: 72 | 73 | ``` 74 | Ответ сервера: 75 | Модель: grok-3 76 | Сообщение: Python — это высокоуровневый, интерпретируемый язык программирования с простым и читаемым синтаксисом. Он поддерживает разные парадигмы программирования (объектно-ориентированное, функциональное, процедурное) и широко используется для веб-разработки, анализа данных, автоматизации задач, машинного обучения и других целей. Python популярен благодаря своей универсальности, обширной стандартной библиотеке и большому сообществу разработчиков. 77 | Причина завершения: stop 78 | Использование: CompletionUsage(completion_tokens=46, prompt_tokens=3, total_tokens=49, completion_tokens_details=None, prompt_tokens_details=None) 79 | ``` 80 | 81 | --- 82 | 83 | 84 | ## 🧵 Эндпоинт: `/v1/string` (GET) 85 | 86 | Простой текстовый GET-запрос. Принимает строку как query-параметр и возвращает ответ от Grok без JSON-обёртки. 87 | 88 | ### 📥 Пример запроса: 89 | 90 | Откройте в браузере или используйте `curl`: 91 | 92 | ```bash 93 | curl http://localhost:9000/v1/string?q=Привет 94 | ``` 95 | 96 | ### 📤 Пример ответа: 97 | 98 | ``` 99 | Привет! Чем могу помочь? 100 | ``` 101 | 102 | ### 🐍 Пример из Python: 103 | 104 | ```python 105 | import requests 106 | 107 | response = requests.get("http://localhost:9000/v1/string", params={"q": "Расскажи анекдот"}) 108 | print(response.text) # Просто текст, без JSON 109 | ``` 110 | 111 | --- 112 | 113 | 114 | ## 🧵 Эндпоинт: `/v1/string` (POST) 115 | 116 | Простой текстовый POST-запрос. Принимает строку в теле запроса и возвращает ответ от Grok без JSON-обёртки. 117 | 118 | ### 📥 Пример запроса: 119 | 120 | Используйте `curl` или другой инструмент: 121 | 122 | ```bash 123 | curl -X POST http://localhost:9000/v1/string -d "Привет" 124 | ``` 125 | 126 | ### 📤 Пример ответа: 127 | 128 | ``` 129 | Привет! Чем могу помочь? 130 | ``` 131 | 132 | ### 🐍 Пример из Python: 133 | 134 | ```python 135 | import requests 136 | 137 | response = requests.post("http://localhost:9000/v1/string", data="Расскажи анекдот") 138 | print(response.text) # Просто текст, без JSON 139 | ``` 140 | 141 | 142 | --- 143 | 144 | ## ⚙️ Переменные окружения (опционально) 145 | 146 | | Переменная | Описание | По умолчанию | 147 | |--------------------|--------------------------------------------|--------------| 148 | | `GROK_COOKIES` | Куки-файл для GrokClient | `None` | 149 | | `GROK_PROXY` | Прокси (например: `http://localhost:8080`) | `None` | 150 | | `GROK_TIMEOUT` | Таймаут запросов Grok (в секундах) | `120` | 151 | | `GROK_SERVER_HOST` | IP для запуска сервера | `0.0.0.0` | 152 | | `GROK_SERVER_PORT` | Порт для запуска сервера | `8000` | 153 | 154 | 155 | 156 | 157 | 158 | --- 159 | 160 | ## 📂 Структура проекта 161 | 162 | ``` 163 | Grok3API/ 164 | ├── grok3api/ 165 | │ ├── server.py # <--- запуск сервера 166 | │ ├── client.py 167 | │ ├── types/ 168 | │ └── ... 169 | ├── tests/ 170 | │ └── openai_test.py # <--- тест совместимости 171 | └── README.md 172 | ``` 173 | 174 | --- 175 | 176 | ## ❗ TODO / Known Issues 177 | 178 | - [ ] 🔄 Поддержка стриминга (`stream=True`) 179 | - [ ] 🧪 Больше тестов и валидации 180 | - [ ] 🧼 Рефакторинг `message_payload` и логики истории 181 | - [ ] 🧩 Кастомные инструкции, изображения и доп. фичи 182 | 183 | --- 184 | 185 | 💬 По вопросам и предложениям — welcome в issues! 186 | ``` -------------------------------------------------------------------------------- /docs/Ru/ClientDoc.md: -------------------------------------------------------------------------------- 1 | # 🛠️ Описание класса `GrokClient` 2 | 3 | ## 🚀 Основной класс для взаимодействия с Grok API. 4 | 5 | Класс `GrokClient` — это основной инструмент для работы с Grok, который используется для отправки запросов к модели и автоматического сохранения истории. 6 | 7 | > 📁 **Работа с историей**: 8 | > При инициализации объекта класса `GrokClient` автоматически инициализируется объект класса `History`. История автоматически подгружается из файла при инициализации `GrokClient`. 9 | 10 | 11 | ### `GrokClient` принимает при инициализации: 12 | 13 | | Параметр | Тип | Описание | По умолчанию | 14 | |---------------------------|-------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|-------------------------| 15 | | `cookies` | `str` / `dict` / `List[str / dict]` | Cookie сайта grok.com (Не обязательно) | `None` | 16 | | `use_xvfb` | `bool` | Флаг для использования Xvfb на Linux. | `True` | 17 | | `proxy` | `str` | URL Прокси сервера, используется только в случае региональной блокировки. | `-` | 18 | | `history_msg_count` | `int` | Количество сообщений в истории. | `0` (история отключена) | 19 | | `history_path` | `str` | Путь к файлу с историей в JSON-формате. | `"chat_histories.json"` | 20 | | `history_as_json` | `bool` | Отправлять ли историю в Grok в формате JSON (если > 0). | `True` | 21 | | `always_new_conversation` | `bool` | Использовать ли url создания нового чата при отправке запроса к Grok. | `True` | 22 | | `conversation_id` | `str` | ID чата grok.com Если хотите продолжить беседу с того места где остановились. Только в паре с response_id. | `None` | 23 | | `response_id` | `str` | ID ответа Grok в чате conversation_id. Если хотите продолжить беседу с того места где остановились. Только в паре с conversation_id. | `None` | 24 | | `enable_artifact_files` | `bool` | Если `False`, то html-объявления файлов будут заменены на markdown-стиль с ` ```{lang} `. | `False` | 25 | | `timeout` | `int` | Максимальное время на инициализацию клиента (в секундах). | `120` | 26 | 27 | --- 28 | 29 | ### 🎯 **Возвращает:** 30 | - Экземпляр класса `GrokClient`, готовый к использованию. 31 | 32 | --- 33 | 34 | 35 | 36 | ### 📋 **Дополнительная информация** 37 | 38 | - 🌐 **Автоматическая инициализация браузера**: При инициализации клиента, сессия Chrome будет запущена автоматически, чтобы подготовить все к отправке запросов. 39 | - 🍪 **Автоматическая смена cookie**: если передан лист cookies (лист строк или словарей), то, в случае достижения лимита сообщений, произойдёт автоматическая смена cookie — новый порядок будет сохранён для текущего и последующих запросов. 40 | - 🐧 **Поддержка Linux**: [Подробное описание работы на Linux](LinuxDoc.md) 41 | 42 | > 💡 На Linux без GUI нужно использовать Xvfb для стабильной работы в headless-режиме. 43 | 44 | > 🛠️ Для начала работы с Grok API создайте экземпляр `GrokClient` и используйте его методы, например, `ChatCompletion.create`, для отправки запросов. 45 | 46 | --- 47 | 48 | ### 🌟 **Пример использования** 49 | 50 | ```python 51 | from grok3api.client import GrokClient 52 | 53 | 54 | def main(): 55 | # Можно добавить лист строк / словарей для автоматической смены в случае достижения лимита 56 | # cookies = "YOUR_COOKIES_FROM_BROWSER" 57 | # client = GrokClient(cookies=cookies) 58 | 59 | # Создание клиента (cookies будут автоматически получены, если не переданы) 60 | client = GrokClient() 61 | 62 | # Отправляем запрос через ChatCompletion 63 | response = client.ask(message="Привет, Grok!") 64 | print(response.modelResponse.message) # Выводит ответ от Grok 65 | 66 | 67 | if __name__ == '__main__': 68 | main() 69 | ``` 70 | 71 | --- 72 | 73 | ### 🔗 **Связанные объекты** 74 | 75 | - **`ChatCompletion`**: Объект, создаваемый внутри `GrokClient`, предоставляет метод `create` для отправки запросов к модели Grok. Подробности смотрите в **[Описании метода `create`](askDoc.md)**. 76 | 77 | --- 78 | 79 | ### 📌 **Примечания** 80 | 81 | - **Обработка ошибок**: При инициализации класса возможны исключения (например, если куки не удалось получить). Они логируются через `logger.error`. 82 | -------------------------------------------------------------------------------- /docs/En/ClientDoc.md: -------------------------------------------------------------------------------- 1 | # 🛠️ Description of the `GrokClient` Class 2 | 3 | ## 🚀 Main class for interacting with the Grok API. 4 | 5 | The `GrokClient` class is the primary tool for working with Grok, used for sending requests to the model and automatically saving the history. 6 | 7 | > 📁 **Working with history**: 8 | > When initializing an object of the `GrokClient` class, an object of the `History` class is automatically initialized. The history is automatically loaded from a file when `GrokClient` is initialized. 9 | 10 | ### `GrokClient` initialization parameters : 11 | 12 | | Parameter | Type | Description | Default | 13 | |---------------------------|-------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------| 14 | | `cookies` | `str` / `dict` / `List[str / dict]` | Cookie from grok.com (not necessary) | `-` | 15 | | `use_xvfb` | `bool` | Flag to use Xvfb on Linux. | `True` | 16 | | `proxy` | `str` | URL of the proxy server, used only in case of regional blocking. | `...` | 17 | | `history_msg_count` | `int` | Number of messages in the history. | `0` (history saving is disabled) | 18 | | `history_path` | `str` | Path to the history file in JSON format. | `"chat_histories.json"` | 19 | | `history_as_json` | `bool` | Whether to send the history to Grok in JSON format (if > 0). | `True` | 20 | | `always_new_conversation` | `bool` | Whether to use the URL for creating a new chat when sending a request to Grok. | `True` | 21 | | `conversation_id` | `str` | Chat ID at grok.com. If you want to continue the conversation from where you left off. Only used together with response_id. | `None` | 22 | | `response_id` | `str` | Grok response ID in the conversation_id chat. If you want to continue the conversation from where you left off. Only used together with conversation_id. | `None` | 23 | | `enable_artifact_files` | `bool` | If `False`, html file declarations will be replaced with markdown style with ` ```{lang}`. | `False` | 24 | | `timeout` | `int` | Maximum time for client initialization (in seconds). | `120` | 25 | 26 | --- 27 | 28 | ### 🎯 **Returns:** 29 | - An instance of the `GrokClient` class, ready for use. 30 | 31 | --- 32 | 33 | 34 | 35 | ### 📋 **Additional information** 36 | 37 | - 🌐 **Automatic Browser Initialization**: When the client is initialized, a Chrome session will automatically start to prepare everything for sending requests. 38 | - 🍪 **Automatic Cookie Rotation**: If a list of cookies (as strings or dictionaries) is provided, cookies will automatically rotate upon reaching the message limit — the new order will be preserved for the current and subsequent requests. 39 | - 🐧 **Linux Support**: [Detailed instructions for running on Linux](LinuxDoc.md) 40 | 41 | > 💡 On Linux without GUI, it is recommended to use Xvfb for stable operation in headless mode. 42 | 43 | > 🛠️ To start working with the Grok API, create an instance of `GrokClient` and use its methods, such as `ChatCompletion.create`, to send requests. 44 | 45 | --- 46 | 47 | ### 🌟 **Example usage** 48 | 49 | ```python 50 | from grok3api.client import GrokClient 51 | 52 | 53 | def main(): 54 | # You can add a list of strings/dictionaries to automatically change when the limit is reached 55 | # client = GrokClient(cookies="YOUR_COOKIES_FROM_BROWSER") 56 | 57 | # Create a client (cookies will be automatically retrieved if not present) 58 | client = GrokClient() 59 | 60 | # Send a request via ChatCompletion 61 | response = client.ask(message="Hello, Grok!") 62 | print(response.modelResponse.message) # Prints the response from Grok 63 | 64 | 65 | if __name__ == '__main__': 66 | main() 67 | ``` 68 | 69 | --- 70 | 71 | ### 🔗 **Related objects** 72 | 73 | - **`ChatCompletion`**: An object created within `GrokClient` that provides the `create` method for sending requests to the Grok model. For details, see **[Description of the `create` method](askDoc.md)**. 74 | 75 | --- 76 | 77 | ### 📌 **Notes** 78 | 79 | - **Error handling**: Exceptions may occur during class initialization (e.g., if cookies could not be obtained). These are logged via `logger.error`. 80 | -------------------------------------------------------------------------------- /grok3api/types/GrokResponse.py: -------------------------------------------------------------------------------- 1 | import re 2 | from dataclasses import dataclass, field 3 | from typing import List, Optional, Any, Dict, Union 4 | 5 | from grok3api.logger import logger 6 | from grok3api.types.GeneratedImage import GeneratedImage 7 | 8 | 9 | @dataclass 10 | class ModelResponse: 11 | responseId: str 12 | message: str 13 | sender: str 14 | createTime: str 15 | parentResponseId: str 16 | manual: bool 17 | partial: bool 18 | shared: bool 19 | query: str 20 | queryType: str 21 | webSearchResults: List[Any] = field(default_factory=list) 22 | xpostIds: List[Any] = field(default_factory=list) 23 | xposts: List[Any] = field(default_factory=list) 24 | generatedImages: List[GeneratedImage] = field(default_factory=list) 25 | imageAttachments: List[Any] = field(default_factory=list) 26 | fileAttachments: List[Any] = field(default_factory=list) 27 | cardAttachmentsJson: List[Any] = field(default_factory=list) 28 | fileUris: List[Any] = field(default_factory=list) 29 | fileAttachmentsMetadata: List[Any] = field(default_factory=list) 30 | isControl: bool = False 31 | steps: List[Any] = field(default_factory=list) 32 | mediaTypes: List[Any] = field(default_factory=list) 33 | 34 | def __init__(self, data: Dict[str, Any], enable_artifact_files: bool): 35 | try: 36 | self.responseId = data.get("responseId", "") 37 | 38 | self.sender = data.get("sender", "") 39 | self.createTime = data.get("createTime", "") 40 | self.parentResponseId = data.get("parentResponseId", "") 41 | self.manual = data.get("manual", False) 42 | self.partial = data.get("partial", False) 43 | self.shared = data.get("shared", False) 44 | self.query = data.get("query", "") 45 | self.queryType = data.get("queryType", "") 46 | self.webSearchResults = data.get("webSearchResults", []) 47 | self.xpostIds = data.get("xpostIds", []) 48 | self.xposts = data.get("xposts", []) 49 | 50 | raw_message = data.get("message", "") 51 | self.message = self._transform_xai_artifacts(raw_message) if not enable_artifact_files else raw_message 52 | 53 | self.generatedImages = [] 54 | for url in data.get("generatedImageUrls", []): 55 | self.generatedImages.append(GeneratedImage(url=url)) 56 | 57 | self.imageAttachments = data.get("imageAttachments", []) 58 | self.fileAttachments = data.get("fileAttachments", []) 59 | self.cardAttachmentsJson = data.get("cardAttachmentsJson", []) 60 | self.fileUris = data.get("fileUris", []) 61 | self.fileAttachmentsMetadata = data.get("fileAttachmentsMetadata", []) 62 | self.isControl = data.get("isControl", False) 63 | self.steps = data.get("steps", []) 64 | self.mediaTypes = data.get("mediaTypes", []) 65 | except Exception as e: 66 | logger.error(f"В ModelResponse.__init__: {str(e)}") 67 | 68 | import re 69 | 70 | def _transform_xai_artifacts(self, text: str) -> str: 71 | """ 72 | Преобразует: 73 | 1. xaiArtifact-блоки с contentType="text/..." → ```\nкод\n``` 74 | 2. Markdown-блоки с языком в виде ```x-src → ``` 75 | 3. Markdown-блоки с языком в виде ```x- → ``` 76 | """ 77 | 78 | # xaiArtifact 79 | def replace_artifact(match): 80 | lang = match.group(1).strip() 81 | code = match.group(2).strip() 82 | return f"```{lang}\n{code}\n```" 83 | 84 | text = re.sub( 85 | r']*?contentType="text/([^"]+)"[^>]*>(.*?)', 86 | replace_artifact, 87 | text, 88 | flags=re.DOTALL 89 | ) 90 | 91 | # ```x-src 92 | text = re.sub( 93 | r'```x-([a-zA-Z0-9_+-]+)src\b', 94 | lambda m: f"```{m.group(1)}", 95 | text 96 | ) 97 | 98 | # ```x- (без src) 99 | text = re.sub( 100 | r'```x-([a-zA-Z0-9_+-]+)\b(?![a-zA-Z0-9_-]*src)', 101 | lambda m: f"```{m.group(1)}", 102 | text 103 | ) 104 | 105 | return text 106 | 107 | 108 | @dataclass 109 | class GrokResponse: 110 | modelResponse: ModelResponse 111 | isThinking: bool 112 | isSoftStop: bool 113 | responseId: str 114 | 115 | conversationId: Optional[str] = None 116 | title: Optional[str] = None 117 | conversationCreateTime: Optional[str] = None 118 | conversationModifyTime: Optional[str] = None 119 | temporary: Optional[bool] = None 120 | 121 | error: Optional[str] = None 122 | error_code: Optional[Union[int, str]] = None 123 | 124 | def __init__(self, data: Dict[str, Any], enable_artifact_files: bool): 125 | try: 126 | self.error = data.get("error", None) 127 | self.error_code = data.get("error_code", None) 128 | result = data.get("result", {}) 129 | response_data = result.get("response", {}) 130 | 131 | self.modelResponse = ModelResponse(response_data.get("modelResponse", {}), enable_artifact_files) 132 | self.isThinking = response_data.get("isThinking", False) 133 | self.isSoftStop = response_data.get("isSoftStop", False) 134 | self.responseId = response_data.get("responseId", "") 135 | 136 | self.conversationId = response_data.get("conversationId") 137 | self.title = result.get("newTitle") or result.get("title") or self.title 138 | self.conversationCreateTime = response_data.get("createTime") 139 | self.conversationModifyTime = response_data.get("modifyTime") 140 | self.temporary = response_data.get("temporary") 141 | except Exception as e: 142 | self.error = str(e) if self.error is None else self.error + ' ' + str(e) 143 | logger.error(f"В GrokResponse.__init__: {e}") 144 | -------------------------------------------------------------------------------- /docs/En/HistoryDoc.md: -------------------------------------------------------------------------------- 1 | # 📚 Description of the `History` class 2 | 3 | ## 🚀 Manages chat history with support for images, system prompts, and saving to JSON 4 | 5 | The `History` class is designed to manage chat histories, including support for system prompts, as well as their saving and loading from a JSON file. 6 | 7 | > ❗ **Important**: 8 | > Grok may misinterpret the history. Experiment with `history_as_json`. You can always disable automatic history saving by setting `history_msg_count` to `0` (by default, `history_msg_count = 0`). 9 | 10 | > ✅ Now, even without using History class, conversation history will be saved on Grok servers by default (old messages will be loaded). However, chats created under different cookies cannot be loaded, which may interfere with using history from Grok servers when automatically managing cookies. History from the History class, when using server-side history, will be added only to the first message of each server conversation (for example, when a new server conversation is created during automatic cookie switching). 11 | 12 | > 📁 **Saving to a file**: 13 | > The history is automatically loaded from the file when initializing `GrokClient`, but you need to save it manually by calling `client.history.to_file`. 14 | 15 | --- 16 | 17 | ### 🌟 Example 18 | 19 | ```python 20 | from grok3api.client import GrokClient 21 | 22 | 23 | def main(): 24 | # Activate auto-saving of history for 5 messages 25 | client = GrokClient(history_msg_count=5) 26 | 27 | # Set the main system prompt 28 | client.history.set_main_system_prompt("Imagine you are a basketball player") 29 | while True: 30 | prompt = input("Enter your query: ") 31 | if prompt == "q": break 32 | result = client.ask(prompt, "0") 33 | print(result.modelResponse.message) 34 | 35 | # Manually save the history to a file 36 | client.history.to_file() 37 | 38 | 39 | if __name__ == '__main__': 40 | main() 41 | ``` 42 | 43 | --- 44 | 45 | ### 📨 **Initialization** 46 | 47 | The `History` class is automatically initialized when creating a `GrokClient` with the following parameters: 48 | 49 | | Parameter | Type | Description | Default | 50 | |---------------------|--------|-------------------------------------------------------------|-------------------------| 51 | | `history_msg_count` | `int` | Maximum number of messages in the chat history | `0` | 52 | | `history_path` | `str` | Path to the JSON file for saving and loading the history | `"chat_histories.json"` | 53 | | `history_as_json` | `bool` | Format of history output: JSON (`True`) or string (`False`) | `True` | 54 | | `history_auto_save` | `bool` | Automatically rewrite history to file after each message. | `True` | 55 | 56 | --- 57 | 58 | ### 🎯 **Attributes** 59 | 60 | | Attribute | Type | Description | 61 | |----------------------|------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 62 | | `chat_histories` | `Dict[str, List[Dict[str, Union[str, List[Dict]]]]]` | Dictionary where keys are history identifiers (`history_id`), and values are lists of messages. Each message contains `role` (sender type) and `content` (list with text and/or images). | 63 | | `history_msg_count` | `int` | Maximum number of messages in the history for each `history_id`. | 64 | | `system_prompts` | `Dict[str, str]` | Dictionary of system prompts, where keys are `history_id`, and values are text prompts for specific histories. | 65 | | `main_system_prompt` | `Optional[str]` | Main system prompt used if no specific prompt is set for a `history_id`. | 66 | | `history_path` | `str` | Path to the JSON file for storing history. | 67 | | `history_as_json` | `bool` | Indicates whether to return the history in JSON format (`True`) or as a string with sender indication (`False`). | 68 | 69 | --- 70 | 71 | ### 📜 **Methods** 72 | 73 | | Method | Parameters | Returns | Description | 74 | |--------------------------|--------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 75 | | `set_main_system_prompt` | `text: str` | - | Sets the main system prompt, which will be used by default. Logs errors via `logger.error`. | 76 | | `set_system_prompt` | `history_id: str`, `text: str` | - | Sets the system prompt for a specific history identifier. Logs errors via `logger.error`. | 77 | | `get_system_prompt` | `history_id: str` | `str` | Returns the system prompt for the specified identifier or an empty string if not set. Logs errors via `logger.error`. | 78 | | `to_file` | - | - | Saves the current data (`chat_histories`, `system_prompts`, `main_system_prompt`) to the JSON file. Data is written with indentation and without forcing ASCII. Logs errors via `logger.error`. | 79 | 80 | --- 81 | 82 | > 💡 **Note**: 83 | > If `history_msg_count = 0`, the history will contain only the system prompt (if present). 84 | 85 | --- 86 | -------------------------------------------------------------------------------- /grok3api/server.py: -------------------------------------------------------------------------------- 1 | # this code is not very well debugged yet, but it seems to work 2 | import argparse 3 | import os 4 | import json 5 | from typing import List, Dict, Optional, Any 6 | 7 | from fastapi import FastAPI, HTTPException, Request 8 | from pydantic import BaseModel 9 | import uvicorn 10 | from starlette.responses import PlainTextResponse 11 | 12 | from grok3api.client import GrokClient 13 | from grok3api.logger import logger 14 | from grok3api.types.GrokResponse import GrokResponse 15 | 16 | 17 | class Message(BaseModel): 18 | role: str 19 | content: str 20 | 21 | class ChatCompletionRequest(BaseModel): 22 | model: str = "grok-3" 23 | messages: List[Message] 24 | temperature: Optional[float] = 1.0 25 | max_tokens: Optional[int] = None 26 | stream: Optional[bool] = False 27 | 28 | class Choice(BaseModel): 29 | index: int 30 | message: Message 31 | finish_reason: str 32 | 33 | class ChatCompletionResponse(BaseModel): 34 | id: str 35 | object: str = "chat.completion" 36 | created: int 37 | model: str 38 | choices: List[Choice] 39 | usage: Dict[str, Any] 40 | 41 | app = FastAPI(title="Grok3API OpenAI-Compatible Server") 42 | 43 | env_cookies = os.getenv("GROK_COOKIES", None) 44 | TIMEOUT = os.getenv("GROK_TIMEOUT", 120) 45 | 46 | try: 47 | grok_client = GrokClient( 48 | cookies=None, 49 | proxy=os.getenv("GROK_PROXY", None), 50 | timeout=TIMEOUT, 51 | history_msg_count=0, 52 | always_new_conversation=True, 53 | ) 54 | except Exception as e: 55 | logger.error(f"Failed to initialize GrokClient: {e}") 56 | raise 57 | 58 | async def handle_grok_str_request(q: str): 59 | if not q.strip(): 60 | raise HTTPException(status_code=400, detail="Query string cannot be empty.") 61 | 62 | response: GrokResponse = await grok_client.async_ask( 63 | message=q, 64 | modelName="grok-3", 65 | timeout=TIMEOUT, 66 | customInstructions="", 67 | disableSearch=False, 68 | enableImageGeneration=False, 69 | enableImageStreaming=False, 70 | enableSideBySide=False 71 | ) 72 | 73 | if response.error or not response.modelResponse.message: 74 | raise HTTPException( 75 | status_code=500, 76 | detail=response.error or "No response from Grok API." 77 | ) 78 | 79 | return response.modelResponse.message 80 | 81 | 82 | @app.get("/v1/string", response_class=PlainTextResponse) 83 | async def simple_string_query_get(q: str): 84 | """ 85 | Простой эндпоинт, принимающий строку как query-параметр и возвращающий ответ от Grok. 86 | Пример: GET /v1/string?q=Привет 87 | """ 88 | return await handle_grok_str_request(q) 89 | 90 | 91 | @app.post("/v1/string", response_class=PlainTextResponse) 92 | async def simple_string_query_post(request: Request): 93 | """ 94 | Простой эндпоинт для POST запроса, принимающий строку как тело и возвращающий ответ от Grok. 95 | Пример: POST /v1/string с телом запроса "Привет" 96 | """ 97 | data = await request.body() 98 | q = data.decode("utf-8").strip() 99 | 100 | return await handle_grok_str_request(q) 101 | 102 | @app.post("/v1/chat/completions") 103 | async def chat_completions( 104 | request: ChatCompletionRequest, 105 | ): 106 | """Эндпоинт для обработки запросов в формате OpenAI.""" 107 | try: 108 | if request.stream: 109 | raise HTTPException(status_code=400, detail="Streaming is not supported.") 110 | 111 | grok_client.cookies = env_cookies 112 | 113 | history_messages = [] 114 | last_user_message = "" 115 | 116 | for msg in request.messages: 117 | if msg.role == "user" and not last_user_message: 118 | last_user_message = msg.content 119 | else: 120 | sender = "USER" if msg.role == "user" else "ASSISTANT" if msg.role == "assistant" else "SYSTEM" 121 | history_messages.append({"sender": sender, "message": msg.content}) 122 | 123 | if history_messages: 124 | history_json = json.dumps(history_messages) 125 | message_payload = f"{history_json}\n{last_user_message}" if last_user_message else history_json 126 | else: 127 | message_payload = last_user_message 128 | 129 | if not message_payload.strip(): 130 | raise HTTPException(status_code=400, detail="No user message provided.") 131 | 132 | response: GrokResponse = await grok_client.async_ask( 133 | message=message_payload, 134 | modelName=request.model, 135 | timeout=TIMEOUT, 136 | customInstructions="", 137 | disableSearch=False, 138 | enableImageGeneration=False, 139 | enableImageStreaming=False, 140 | enableSideBySide=False 141 | ) 142 | 143 | if response.error or not response.modelResponse.message: 144 | raise HTTPException( 145 | status_code=500, 146 | detail=response.error or "No response from Grok API." 147 | ) 148 | 149 | import time 150 | current_time = int(time.time()) 151 | response_id = response.responseId or f"chatcmpl-{current_time}" 152 | 153 | chat_response = ChatCompletionResponse( 154 | id=response_id, 155 | created=current_time, 156 | model=request.model, 157 | choices=[ 158 | Choice( 159 | index=0, 160 | message=Message( 161 | role="assistant", 162 | content=response.modelResponse.message 163 | ), 164 | finish_reason="stop" 165 | ) 166 | ], 167 | usage={ 168 | "prompt_tokens": len(message_payload.split()), 169 | "completion_tokens": len(response.modelResponse.message.split()), 170 | "total_tokens": len(message_payload.split()) + len(response.modelResponse.message.split()) 171 | } 172 | ) 173 | 174 | return chat_response 175 | 176 | except Exception as ex: 177 | logger.error(f"Error in chat_completions: {ex}") 178 | raise HTTPException(status_code=500, detail=str(ex)) 179 | 180 | def run_server(default_host: str = "0.0.0.0", default_port: int = 8000): 181 | parser = argparse.ArgumentParser(description="Run Grok3API-compatible server.") 182 | parser.add_argument( 183 | "--host", 184 | type=str, 185 | default=os.getenv("GROK_SERVER_HOST", default_host), 186 | help="Host to bind the server to (default: env GROK_SERVER_HOST or 0.0.0.0)" 187 | ) 188 | parser.add_argument( 189 | "--port", 190 | type=int, 191 | default=int(os.getenv("GROK_SERVER_PORT", default_port)), 192 | help="Port to bind the server to (default: env GROK_SERVER_PORT or 8000)" 193 | ) 194 | 195 | args = parser.parse_args() 196 | 197 | print(f"🚀 Starting server on {args.host}:{args.port}") 198 | uvicorn.run(app, host=args.host, port=args.port) 199 | 200 | if __name__ == "__main__": 201 | run_server() -------------------------------------------------------------------------------- /docs/Ru/HistoryDoc.md: -------------------------------------------------------------------------------- 1 | # 📚 Описание класса `History` 2 | 3 | ## 🚀 Управляет историей чата с поддержкой изображений, системными промптами и сохранением в JSON 4 | 5 | Класс `History` предназначен для управления историей чатов, включая поддержку системных промптов, а также их сохранения и загрузки из файла JSON. 6 | 7 | > ❗ **Важно**: 8 | > Grok может неправильно воспринимать историю. Поэкспериментируйте с `history_as_json`. Всегда можно отключить автоматическое сохранение истории, установив `history_msg_count` на `0` (по умолчанию и так `history_msg_count = 0`). 9 | 10 | > ✅ Теперь даже без использования класса History, по умолчанию будет сохраняться история чата на серверах Grok (будут подгружаться старые сообщения). Однако, чаты, созданные на других cookies подгрузить невозможно, что может помешать использовать историю из серверов Grok при автоматическом управлении cookies. История из класса History при использовании истории из сервера будут добавлена только к первому сообщению каждого серверного чата (например при авто-смене cookies создаётся новый серверный чат). 11 | 12 | > 📁 **Сохранение в файл**: 13 | > История автоматически подгружается из файла при инициализации `GrokClient`, но сохранять необходимо вручную, вызывая `client.history.to_file`. 14 | 15 | --- 16 | 17 | ### 🌟 Пример 18 | 19 | ```python 20 | from grok3api.client import GrokClient 21 | 22 | 23 | def main(): 24 | # Активируем авто-сохранение истории для 5 сообщений 25 | client = GrokClient(history_msg_count=5) 26 | 27 | # Устанавливаем общий системный промпт 28 | client.history.set_main_system_prompt("Представь что ты баскетболист") 29 | while True: 30 | prompt = input("Ведите запрос: ") 31 | if prompt == "q": break 32 | result = client.ask(prompt, "0") 33 | print(result.modelResponse.message) 34 | 35 | # Вручную сохраняем историю в файл 36 | client.history.to_file() 37 | 38 | 39 | if __name__ == '__main__': 40 | main() 41 | ``` 42 | --- 43 | 44 | ### 📨 **Инициализация** 45 | 46 | Класс `History` инициализируется **автоматически при создании GrokClient** со следующими параметрами: 47 | 48 | | Параметр | Тип | Описание | По умолчанию | 49 | |---------------------|--------|-------------------------------------------------------------------|-------------------------| 50 | | `history_msg_count` | `int` | Максимальное количество сообщений в истории чата | `0` | 51 | | `history_path` | `str` | Путь к файлу JSON для сохранения и загрузки истории | `"chat_histories.json"` | 52 | | `history_as_json` | `bool` | Формат вывода истории: JSON (`True`) или строка (`False`) | `True` | 53 | | `history_auto_save` | `bool` | Автоматическая перезапись истории в файл после каждого сообщения. | `True` | 54 | 55 | 56 | --- 57 | 58 | ### 🎯 **Атрибуты** 59 | 60 | | Атрибут | Тип | Описание | 61 | |----------------------|------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 62 | | `chat_histories` | `Dict[str, List[Dict[str, Union[str, List[Dict]]]]]` | Словарь, где ключи — идентификаторы истории (`history_id`), а значения — списки сообщений. Каждое сообщение содержит `role` (тип отправителя) и `content` (список с текстом и/или изображениями). | 63 | | `history_msg_count` | `int` | Максимальное количество сообщений в истории для каждого `history_id`. | 64 | | `system_prompts` | `Dict[str, str]` | Словарь системных промптов, где ключи — `history_id`, а значения — текстовые промпты для конкретных историй. | 65 | | `main_system_prompt` | `Optional[str]` | Основной системный промпт, применяемый, если для `history_id` не задан специфический промпт. | 66 | | `history_path` | `str` | Путь к файлу JSON для хранения истории. | 67 | | `history_as_json` | `bool` | Указывает, возвращать ли историю в формате JSON (`True`) или как строку с указанием отправителя (`False`). | 68 | 69 | --- 70 | 71 | ### 📜 **Методы** 72 | 73 | | Метод | Параметры | Возвращает | Описание | 74 | |--------------------------|-------------------------------------------------------------------------------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 75 | | `set_main_system_prompt` | `text: str` | - | Устанавливает основной системный промпт, который будет использоваться по умолчанию. При возникновении ошибки логирует исключение через `logger.error`. | 76 | | `set_system_prompt` | `history_id: str`, `text: str` | - | Устанавливает системный промпт для конкретного идентификатора истории. Ошибки логируются через `logger.error`. | 77 | | `get_system_prompt` | `history_id: str` | `str` | Возвращает системный промпт для указанного идентификатора или пустую строку, если промпт не установлен. Ошибки логируются через `logger.error`. | 78 | | `to_file` | - | - | Сохраняет текущие данные (`chat_histories`, `system_prompts`, `main_system_prompt`) в файл JSON. Данные записываются с отступами и без принудительного использования ASCII. Ошибки логируются через `logger.error`. | 79 | 80 | --- 81 | 82 | > 💡 **Примечание**: 83 | > Если `history_msg_count = 0`, история будет содержать только системный промпт (при его наличии). 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 🚀 A Python library for interacting with the Grok 3 API without requiring login or manual cookie input. Perfect for out-of-the-box use. 2 | 3 | 4 | 5 | ## [➡ Ru ReadMe](docs/Ru/RuReadMe.md) 6 | 7 | # 🤖 Grok3API: Client for Working with Grok 8 | 9 | ![Python](https://img.shields.io/badge/python-3.8%2B-blue?logo=python&logoColor=white) 10 | ![Made with ❤️](https://img.shields.io/badge/Made%20with-%F0%9F%92%9C-red) 11 | 12 | ![Stars](https://img.shields.io/github/stars/boykopovar/Grok3API?style=social) 13 | ![Forks](https://img.shields.io/github/forks/boykopovar/Grok3API?style=social) 14 | ![Issues](https://img.shields.io/github/issues/boykopovar/Grok3API?style=social) 15 | 16 | 17 | **Grok3API** is a powerful and user-friendly unofficial tool for interacting with Grok models (including Grok3), allowing you to send requests, receive text responses, and, most excitingly, **generated images** — all with automatic cookie management! 🎨✨ The project is designed with simplicity and automation in mind, so you can focus on creativity rather than technical details. 18 | 19 | 20 | --- 21 | 22 | ## [📦 Full Changelog](docs/En/ChangeLog.md) 23 | 24 | ### 🆕 v0.1.0b1 25 | 26 | #### ✨ New: 27 | 28 | * 🛠 **Improved code block handling** 29 | Added automatic transformation of nested blocks `...` into standard Markdown code blocks with language indication. 30 | 31 | * ☑️ The feature can be disabled by setting the `auto_transform_code_blocks=False` parameter when creating `GrokClient`. 32 | 33 | 34 | --- 35 | 36 | 37 | 38 | ## 🌟 Features 39 | 40 | - 🚀 **Automatic cookie retrieval** via browser with Cloudflare bypass — no manual setup required! 41 | - 🖼️ **Convenient retrieval of generated images** with the `save_to` method, enabling one-click saving. 42 | - 🔧 **Flexible request customization**: model selection, image generation control, attachment support, and more. 43 | - 📦 **Attachment support**: send files and images along with requests. 44 | - 🛠️ **Error handling**: the client automatically resolves cookie issues and retries requests if something goes wrong. 45 | - 🤖 **[Example Telegram bot](tests/SimpleTgBot/SimpleTgBot.py) (`grok3api` + `aiogram`)** 46 | --- 47 | 48 | ## 📦 Installation 49 | 50 | To start using GrokClient, install the required dependencies. It’s simple: 51 | 52 | ```bash 53 | pip install grok3api 54 | ``` 55 | 56 | > ⚠️ **Important**: Ensure **Google Chrome** is installed, as `undetected_chromedriver` relies on it. 57 | 58 | After installation, you’re ready to go! 🎉 59 | 60 | --- 61 | 62 | ## 🚀 Usage 63 | 64 | ### Quick Start 65 | 66 | 🍀 Minimal working example: 67 | ```python 68 | from grok3api.client import GrokClient 69 | 70 | client = GrokClient() 71 | answer = client.ask("Hi! How are you?") 72 | 73 | print(answer.modelResponse.message) 74 | ``` 75 | 76 | 77 | Here’s a complete example of sending a request and saving a generated image: 78 | 79 | ```python 80 | from grok3api.client import GrokClient 81 | 82 | # Create a client (cookies are automatically retrieved if not provided) 83 | client = GrokClient() 84 | 85 | # Create a request 86 | message = "Create an image of a ship" 87 | 88 | # Send the request 89 | result = client.ask(message=message, 90 | images="C:\\Folder\\photo1_to_grok.jpg") # You can send an image to Grok 91 | 92 | print("Grok's response:", result.modelResponse.message) 93 | 94 | # Save the first image, if available 95 | if result.modelResponse.generatedImages: 96 | result.modelResponse.generatedImages[0].save_to("ship.jpg") 97 | print("Image saved as ship.jpg! 🚀") 98 | ``` 99 | 100 | This code: 101 | 1. **Creates a client** — automatically retrieves cookies if none are provided. 102 | 2. **Sends a request** to generate an image. 103 | 3. **Saves the image** to the file `ship.jpg`. 104 | 105 | 📌 **What will we see?** 106 | Grok will generate an image of a **ship**, for example, something like this: 107 | 108 | Ship example 109 | 110 | 🐹 Or, for instance, if you request "**A gopher on Elbrus**": 111 | 112 | Gopher on Elbrus 113 | 114 | > 💡 **Cool feature**: You don’t need to manually fetch cookies — the client handles it for you! 115 | 116 | --- 117 | 118 | ## 🔧 Request Parameters 119 | 120 | The `GrokClient.ask` method accepts various parameters to customize your request. Here’s an example with settings: 121 | 122 | ```python 123 | from grok3api.client import GrokClient 124 | 125 | 126 | client = GrokClient(history_msg_count=5, always_new_conversation=False) # to use conversation history from grok.com 127 | client.history.set_main_system_prompt("Respond briefly and with emojis.") 128 | 129 | # Send a request with settings 130 | result = client.ask( 131 | message="Draw a cat like in this picture", 132 | modelName="grok-3", # Default is grok-3 anyway 133 | images=["C:\\Users\\user\\Downloads\\photo1_to_grok.jpg", 134 | "C:\\Users\\user\\Downloads\\photo2_to_grok.jpg"] # You can send multiple images to Grok! 135 | ) 136 | print(f"Grok3 response: {result.modelResponse.message}") 137 | 138 | # Save all images 139 | for i, img in enumerate(result.modelResponse.generatedImages): 140 | img.save_to(f"cat_{i}.jpg") 141 | print(f"Saved: cat_{i}.jpg 🐾") 142 | ``` 143 | 144 | > 🌟 **The best part? It works with automatically retrieved cookies!** No need to worry about access — the client sets everything up for you. 145 | 146 | --- 147 | 148 | ## 🔄 Automatic Cookie Retrieval 149 | 150 | If cookies are missing or expired, Grok3API automatically: 151 | 1. Uses the Chrome browser (ensure it’s installed). 152 | 2. Visits `https://grok.com/`. 153 | 3. Bypasses Cloudflare protection. 154 | 4. Continues operation. 155 | 156 | You don’t need to do anything manually — just run the code, and it works! 157 | 158 | ### [💼️ `GrokClient` Class Description](docs/En/ClientDoc.md) 159 | ### [✈️ `ask` Method Description](docs/En/askDoc.md) 160 | ### [📋 `History` Class Description](docs/En/HistoryDoc.md) 161 | ### [📬 `GrokResponse` Class Description](docs/En/GrokResponse.md) 162 | ### [🐧 Working with `Linux`](docs/En/LinuxDoc.md) 163 | ### [🌐 Running an OpenAI-Compatible Server](docs/En/OpenAI_Server.md) 164 | 165 | --- 166 | 167 | ## 🖼️ Convenient Image Retrieval 168 | 169 | One of the standout features of GrokClient is its **super-convenient handling of generated images**. Here’s a complete example: 170 | 171 | ```python 172 | from grok3api.client import GrokClient 173 | 174 | client = GrokClient() 175 | result = client.ask("Draw a sunset over the sea") 176 | 177 | for i, image in enumerate(result.modelResponse.generatedImages): 178 | image.save_to(f"sunset_{i}.jpg") 179 | print(f"Saved: sunset_{i}.jpg 🌅") 180 | ``` 181 | 182 | --- 183 | 184 | ## 📋 Response Handling 185 | 186 | The `ask` method returns a `GrokResponse` object. 187 | 188 | Fields of the `GrokResponse` object: 189 | - **`modelResponse`**: The main model response. 190 | - `message` (str): The text response. 191 | - `generatedImages` (List[GeneratedImage]): List of images. 192 | - **`isThinking`**: Whether the model was thinking (bool). 193 | - **`isSoftStop`**: Soft stop (bool). 194 | - **`responseId`**: Response ID (str). 195 | - **`newTitle`**: New chat title, if available (Optional[str]). 196 | 197 | ### [📬 Detailed `GrokResponse` Class Description](docs/En/GrokResponse.md) 198 | 199 | --- 200 | 201 | If something’s unclear, feel free to raise an issue — we’ll figure it out together! 🌟 202 | 203 | ## Disclaimer 204 | Grok3API has no affiliation with xAI or the Grok developers. It is an independent project created by a third party and is not supported, sponsored or endorsed by xAI. Any issues with Grok should be addressed directly to xAI. 205 | You are responsible for ensuring that your use of Grok3API complies with all applicable laws and regulations. The developer does not encourage illegal use. 206 | -------------------------------------------------------------------------------- /docs/Ru/RuReadMe.md: -------------------------------------------------------------------------------- 1 | 🚀 Библиотека Python для взаимодействия с API Grok 3 без необходимости входа в систему или необходимости ввода файлов cookie. Идеально подходит для использования «из коробки». 2 | ## [➡ English ReadMe](../../README.md) 3 | 4 | # 🤖 Grok3API: Клиент для работы с Grok 5 | 6 | ![Python](https://img.shields.io/badge/python-3.8%2B-blue?logo=python&logoColor=white) 7 | ![Made with ❤️](https://img.shields.io/badge/Made%20with-%F0%9F%92%9C-red) 8 | 9 | ![Stars](https://img.shields.io/github/stars/boykopovar/Grok3API?style=social) 10 | ![Forks](https://img.shields.io/github/forks/boykopovar/Grok3API?style=social) 11 | ![Issues](https://img.shields.io/github/issues/boykopovar/Grok3API?style=social) 12 | 13 | 14 | **Grok3API** — это мощный и удобный неофициальный инструмент для взаимодействия с моделями Grok (включая Grok3), который позволяет отправлять запросы, получать текстовые ответы и, что особенно круто, **сгенерированные изображения** — всё это с автоматическим управлением cookies! 🎨✨ Проект разработан с упором на простоту использования и автоматизацию, чтобы вы могли сосредоточиться на творчестве, а не на технических деталях. 15 | 16 | 17 | --- 18 | 19 | ## [📦 Подробный Changelog](ChangeLog.md) 20 | 21 | ### 🆕 v0.1.0b1 22 | 23 | #### ✨ Новое: 24 | 25 | * 🛠 **Улучшена обработка код-блоков** 26 | Добавлена автоматическая трансформация вложенных блоков `...` в стандартные Markdown-блоки с указанием языка. 27 | 28 | * ☑️ Функцию можно отключить, указав параметр `auto_transform_code_blocks=False` при создании `GrokClient`. 29 | 30 | --- 31 | 32 | 33 | 34 | ## 🌟 Возможности 35 | 36 | - 🚀 **Автоматическое получение cookies** через браузер с обходом Cloudflare — никаких ручных настроек! 37 | - 🖼️ **Удобное получение сгенерированных изображений** с методом `save_to`, который позволяет сохранить их в один клик. 38 | - 🔧 **Гибкая настройка запросов**: выбор модели, управление генерацией изображений, добавление вложений и многое другое. 39 | - 📦 **Поддержка вложений**: отправляйте файлы и изображения вместе с запросами. 40 | - 🛠️ **Обработка ошибок**: клиент сам решает проблемы с cookies и повторяет запросы, если что-то пошло не так. 41 | - 🤖 **[Пример Telegram бота](../../tests/SimpleTgBot/SimpleTgBot.py) (`grok3api` + `aiogram`)** 42 | --- 43 | 44 | ## 📦 Установка 45 | 46 | Чтобы начать использовать GrokClient, установите необходимые зависимости. Всё просто: 47 | 48 | ```bash 49 | pip install grok3api 50 | ``` 51 | 52 | > ⚠️ **Важно**: Убедитесь, что у вас установлен **Google Chrome**, так как `undetected_chromedriver` работает именно с ним. 53 | 54 | После установки вы готовы к запуску! 🎉 55 | 56 | --- 57 | 58 | ## 🚀 Использование 59 | 60 | 61 | ### Быстрый старт 62 | 63 | 🍀 Минимальный рабочий пример: 64 | 65 | ```python 66 | from grok3api.client import GrokClient 67 | 68 | grok = GrokClient() 69 | answer = grok.ask("Привет! Как дела?") 70 | 71 | print(answer.modelResponse.message) 72 | ``` 73 | 74 | Вот полный пример, как отправить запрос и сохранить сгенерированное изображение: 75 | 76 | ```python 77 | from grok3api.client import GrokClient 78 | 79 | # Создание клиента (cookies будут автоматически получены, если не переданы) 80 | client = GrokClient() 81 | 82 | # Создаём запрос 83 | message = "Создай изображение корабля" 84 | 85 | # Отправляем запрос 86 | result = client.ask(message=message, 87 | images="C:\\Users\\user\\Downloads\\photo1_to_grok.jpg") # Вы можете отправлять картинку Гроку 88 | 89 | print("Ответ Grok:", result.modelResponse.message) 90 | 91 | # Сохраняем первое изображение, если оно есть 92 | if result.modelResponse.generatedImages: 93 | result.modelResponse.generatedImages[0].save_to("ship.jpg") 94 | print("Изображение сохранено как ship.jpg! 🚀") 95 | ``` 96 | 97 | Этот код: 98 | 1. **Создаёт клиента** — автоматически получает cookies, если их нет. 99 | 2. **Отправляет запрос** на генерацию изображения. 100 | 3. **Сохраняет изображение** в файл `ship.jpg`. 101 | 102 | 📌 **Что мы увидим?** 103 | Grok сгенерирует изображение **корабля**, например, вот такое: 104 | 105 | Пример корабля 106 | 107 | 🐹 Или, например, вы попросите "**Суслика на Эльбрусе**": 108 | 109 | Суслик на Эльбрусе 110 | 111 | > 💡 **Фишка**: Вам не нужно вручную добывать cookies — клиент сделает это за вас! 112 | 113 | 114 | --- 115 | 116 | ## 🔧 Параметры запроса 117 | 118 | Метод `GrokClient.ask` принимает множество параметров для настройки вашего запроса. Вот пример с настройками: 119 | 120 | ```python 121 | from grok3api.client import GrokClient 122 | 123 | 124 | client = GrokClient(history_msg_count=5, always_new_conversation=False) # для использования истории чата из серверов grok.com 125 | client.history.set_main_system_prompt("Отвечай коротко и с эмодзи.") 126 | 127 | # Отправляем запрос с настройками 128 | result = client.ask( 129 | message="Нарисуй кота как на это картинке", 130 | modelName="grok-3", # По умолчанию и так grok-3 131 | images=["C:\\Users\\user\\Downloads\\photo1_to_grok.jpg", 132 | "C:\\Users\\user\\Downloads\\photo2_to_grok.jpg"] # Вы можете отправлять несколько изображений Гроку! 133 | ) 134 | print(f"Ответ Grok3: {result.modelResponse.message}") 135 | 136 | # Сохраняем все изображения 137 | for i, img in enumerate(result.modelResponse.generatedImages): 138 | img.save_to(f"cat_{i}.jpg") 139 | print(f"Сохранено: cat_{i}.jpg 🐾") 140 | ``` 141 | 142 | > 🌟 **Круто то, что это работает с автоматически полученными cookies!** Вам не нужно беспокоиться о доступе — клиент сам всё настроит. 143 | 144 | --- 145 | 146 | ## 🔄 Автоматическое получение cookies 147 | 148 | Если cookies отсутствуют или устарели, Grok3API автоматически: 149 | 1. Использует браузер Chrome (главное, чтобы он был установлен). 150 | 2. Посетит сайт `https://grok.com/`. 151 | 3. Обойдёт защиту Cloudflare. 152 | 4. Продолжит работу. 153 | 154 | Вам не нужно ничего делать вручную — просто запустите код, и всё заработает! 155 | 156 | ### [💼️ Описание класса `GrokClient`](ClientDoc.md) 157 | ### [✈️ Описание метода `ask`](askDoc.md) 158 | ### [📋 Описание класса `History`](HistoryDoc.md) 159 | ### [📬 Описание класса `GrokResponse`](GrokResponse.md) 160 | ### [🐧 Особенности работы с `Linux`](LinuxDoc.md) 161 | ### [🌐 Запуск OpenAI-совместимого сервера](OpenAI_Server.md) 162 | 163 | --- 164 | 165 | ## 🖼️ Удобное получение изображений 166 | 167 | Одна из главных фишек GrokClient — это **супер-удобная работа со сгенерированными изображениями**. Вот полный пример: 168 | 169 | ```python 170 | from grok3api.client import GrokClient 171 | 172 | client = GrokClient() 173 | result = client.ask("Нарисуй закат над морем") 174 | 175 | for i, image in enumerate(result.modelResponse.generatedImages): 176 | image.save_to(f"sunset_{i}.jpg") 177 | print(f"Сохранено: sunset_{i}.jpg 🌅") 178 | ``` 179 | 180 | 181 | --- 182 | 183 | 184 | ## 📋 Обработка ответов 185 | 186 | Метод `ask` возвращает объект `GrokResponse`. 187 | 188 | Поля объекта `GrokResponse`: 189 | - **`modelResponse`**: Основной ответ модели. 190 | - `message` (str): Текстовый ответ. 191 | - `generatedImages` (List[GeneratedImage]): Список изображений. 192 | - **`isThinking`**: Думала ли модель (bool). 193 | - **`isSoftStop`**: Мягкая остановка (bool). 194 | - **`responseId`**: ID ответа (str). 195 | - **`newTitle`**: Новый заголовок чата, если есть (Optional[str]). 196 | 197 | ### [📬 Подробное описание класса `GrokResponse`](GrokResponse.md) 198 | 199 | --- 200 | 201 | 202 | Если что-то неясно, пишите в issues — разберёмся вместе! 🌟 203 | 204 | 205 | ## Дисклеймер 206 | Grok3API не имеет связи с xAI или разработчиками Grok. Это независимый проект, созданный третьей стороной, и он не поддерживается, не спонсируется и не одобряется xAI. Любые проблемы, связанные с Grok, должны решаться напрямую с xAI. 207 | Вы несете ответственность за то, чтобы ваше использование Grok3API соответствовало всем применимым законам и регулированиям. Разработчик не поощряет незаконное использование. -------------------------------------------------------------------------------- /grok3api/history.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from typing import Dict, List, Optional, Union 4 | from enum import Enum 5 | import base64 6 | from io import BytesIO 7 | import imghdr 8 | 9 | from grok3api.logger import logger 10 | 11 | try: 12 | import aiofiles 13 | AIOFILES_AVAILABLE = True 14 | except ImportError: 15 | AIOFILES_AVAILABLE = False 16 | 17 | class SenderType(Enum): 18 | USER = "user" 19 | ASSISTANT = "assistant" 20 | SYSTEM = "system" 21 | 22 | class History: 23 | def __init__(self, 24 | history_msg_count: int = 0, 25 | history_path: str = "chat_histories.json", 26 | history_as_json: bool = True, 27 | main_system_prompt: str = None): 28 | self._chat_histories: Dict[str, List[Dict[str, Union[str, List[Dict[str, str]]]]]] = {} 29 | self.history_msg_count = history_msg_count 30 | self._system_prompts: Dict[str, str] = {} 31 | self.main_system_prompt: Optional[str] = main_system_prompt 32 | self.history_path = history_path 33 | self.history_as_json = history_as_json 34 | self.from_file() 35 | 36 | def set_main_system_prompt(self, text: str): 37 | try: 38 | self.main_system_prompt = text 39 | except Exception as e: 40 | logger.error(f"В set_main_system_prompt: {e}") 41 | 42 | def add_message(self, history_id: str, 43 | sender_type: SenderType, 44 | message: str): 45 | try: 46 | if self.history_msg_count < 0: 47 | self.history_msg_count = 0 48 | if history_id not in self._chat_histories: 49 | self._chat_histories[history_id] = [] 50 | 51 | content = [] 52 | if message: 53 | content.append({"type": "text", "text": message}) 54 | 55 | new_message = {'role': sender_type.value, 'content': content} 56 | self._chat_histories[history_id].append(new_message) 57 | 58 | max_messages = self.history_msg_count + 1 59 | if len(self._chat_histories[history_id]) > max_messages: 60 | self._chat_histories[history_id] = self._chat_histories[history_id][-max_messages:] 61 | except Exception as e: 62 | logger.error(f"В add_message: {e}") 63 | 64 | def get_history(self, history_id: str) -> str: 65 | try: 66 | history = self._chat_histories.get(history_id, [])[:self.history_msg_count] 67 | 68 | if history_id not in self._system_prompts and self.main_system_prompt: 69 | history = [{'role': SenderType.SYSTEM.value, 'content': [{"type": "text", "text": self.main_system_prompt}]}] + history 70 | elif history_id in self._system_prompts: 71 | history = [{'role': SenderType.SYSTEM.value, 'content': [{"type": "text", "text": self._system_prompts[history_id]}]}] + history 72 | 73 | if self.history_as_json: 74 | return json.dumps(history, ensure_ascii=False) 75 | 76 | formatted_messages = [] 77 | for msg in history: 78 | content = msg.get('content', '') 79 | text = '' 80 | 81 | if isinstance(content, str): 82 | text = content 83 | elif isinstance(content, list): 84 | for item in content: 85 | if isinstance(item, dict) and item.get('type') == 'text': 86 | text = item.get('text', '') 87 | break 88 | 89 | formatted_line = f"{msg['role']}: {text}" 90 | formatted_messages.append(formatted_line) 91 | return "\n".join(formatted_messages) 92 | 93 | except Exception as e: 94 | logger.error(f"В get_history: {e}") 95 | return [] if self.history_as_json else "" 96 | 97 | def set_system_prompt(self, history_id: str, text: str): 98 | try: 99 | self._system_prompts[history_id] = text 100 | except Exception as e: 101 | logger.error(f"В set_system_prompt: {e}") 102 | 103 | def get_system_prompt(self, history_id: str) -> str: 104 | try: 105 | return self._system_prompts.get(history_id, "") 106 | except Exception as e: 107 | logger.error(f"В get_system_prompt: {e}") 108 | return "" 109 | 110 | def del_history_by_id(self, history_id: str) -> bool: 111 | """Deletes the chat history by `history_id`.""" 112 | try: 113 | if history_id in self._chat_histories: 114 | del self._chat_histories[history_id] 115 | 116 | if history_id in self._system_prompts: 117 | del self._system_prompts[history_id] 118 | 119 | logger.debug(f"История с ID {history_id} удалена.") 120 | return True 121 | except Exception as e: 122 | logger.error(f"В delete_history: {e}") 123 | return False 124 | 125 | def to_file(self): 126 | try: 127 | with open(self.history_path, "w", encoding="utf-8") as file: 128 | json.dump({ 129 | "chat_histories": self._chat_histories, 130 | "system_prompts": self._system_prompts, 131 | "main_system_prompt": self.main_system_prompt 132 | }, file, ensure_ascii=False, indent=4) 133 | except Exception as e: 134 | logger.error(f"В save_history: {e}") 135 | 136 | async def async_to_file(self): 137 | """Asynchronously saves data to a file in JSON format.""" 138 | try: 139 | data = { 140 | "chat_histories": self._chat_histories, 141 | "system_prompts": self._system_prompts, 142 | "main_system_prompt": self.main_system_prompt 143 | } 144 | if AIOFILES_AVAILABLE: 145 | async with aiofiles.open(self.history_path, "w", encoding="utf-8") as file: 146 | await file.write(json.dumps(data, ensure_ascii=False, indent=4)) 147 | else: 148 | def write_file_sync(path: str, content: dict): 149 | with open(path, "w", encoding="utf-8") as file: 150 | json.dump(content, file, ensure_ascii=False, indent=4) 151 | 152 | await asyncio.to_thread(write_file_sync, self.history_path, data) 153 | except Exception as e: 154 | logger.error(f"В to_file: {e}") 155 | 156 | def from_file(self): 157 | try: 158 | with open(self.history_path, "r", encoding="utf-8") as file: 159 | data = json.load(file) 160 | self._chat_histories = data.get("chat_histories", {}) 161 | self._system_prompts = data.get("system_prompts", {}) 162 | self.main_system_prompt = data.get("main_system_prompt", None) 163 | except FileNotFoundError: 164 | logger.debug("В load_history: Файл не найден.") 165 | except Exception as e: 166 | logger.error(f"В load_history: {e}") 167 | 168 | 169 | def encode_image(image: Union[str, BytesIO]) -> Optional[tuple[str, str]]: 170 | """Encodes an image in base64 and determines its type.""" 171 | try: 172 | if isinstance(image, str): 173 | with open(image, "rb") as image_file: 174 | image_data = image_file.read() 175 | elif isinstance(image, BytesIO): 176 | image_data = image.getvalue() 177 | else: 178 | raise ValueError("Изображение должно быть путем к файлу или объектом BytesIO") 179 | 180 | image_type = imghdr.what(None, h=image_data) 181 | if not image_type: 182 | image_type = "jpeg" 183 | 184 | base64_image = base64.b64encode(image_data).decode('utf-8') 185 | return base64_image, image_type 186 | except FileNotFoundError: 187 | logger.error(f"В encode_image: Файл {image} не найден.") 188 | return None 189 | except Exception as e: 190 | logger.error(f"В encode_image: {e}") 191 | return None -------------------------------------------------------------------------------- /docs/En/askDoc.md: -------------------------------------------------------------------------------- 1 | # Descriptions of the `ask` method 2 | 3 | ## 🚀 Sends a request to the Grok API and receives a response. There is an asynchronous variant `async_ask`. 4 | 5 | ### 📨 **Takes:** 6 | - 📜 `message`: The text of the request for the model. 7 | - ⚙ `**kwargs`: Additional parameters for configuration. 8 | 9 | ### 🎯 **Returns:** 10 | - `GrokResponse` – an object containing the response from the Grok API. 11 | - **[Description of the `GrokResponse`](GrokResponse.md)** 12 | 13 | ### Complete Parameter List: 14 | 15 | | Parameter | Type | Description | Default | 16 | |-----------------------------|-----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|------------| 17 | | `message` | `str` | **Required**. The request text for Grok. | - | 18 | | `history_id` | `str` | Identifier in the history dictionary (if auto saving history is used) | `None` | 19 | | `new_conversation` | `bool` | Whether to use the new chat URL when sending a request to Grok (does not affect the built-in History class). | `False` | 20 | | `timeout` | `int` | Timeout for one wait to receive a response (seconds). | `45` | 21 | | `temporary` | `bool` | Indicates if the request or session is temporary. | `False` | 22 | | `modelName` | `str` | Name of the AI model (e.g., "grok-3"). | `"grok-3"` | 23 | | `images` | `str_path` / `str_base64` / `BytesIO` / `List[str]` / `List[BytesIO]` | Either the path to an image, a base64-encoded image, or a BytesIO (or a list of any of these) to send. fileAttachments must not be used. | `None` | 24 | | `fileAttachments` | `List[str]` | List of file attachments (keys: `name`, `content`). | `[]` | 25 | | `imageAttachments` | `List[]` | List of image attachments (keys: `name`, `content`). | `[]` | 26 | | `customInstructions` | `str` | Additional instructions for the model. | `""` | 27 | | `deepsearch_preset` | `str` | Preset for deep search. | `""` | 28 | | `disableSearch` | `bool` | Disable search in model responses. | `False` | 29 | | `enableImageGeneration` | `bool` | Enable image generation. | `True` | 30 | | `enableImageStreaming` | `bool` | Enable image streaming. | `True` | 31 | | `enableSideBySide` | `bool` | Enable side-by-side information display. | `True` | 32 | | `imageGenerationCount` | `int` | Number of images to generate. | `4` | 33 | | `isPreset` | `bool` | Indicates if the message is preset. | `False` | 34 | | `isReasoning` | `bool` | Enable reasoning mode for the model. | `False` | 35 | | `returnImageBytes` | `bool` | Return images as bytes. | `False` | 36 | | `returnRawGrokInXaiRequest` | `bool` | Return raw model output. | `False` | 37 | | `sendFinalMetadata` | `bool` | Send final metadata with the request. | `True` | 38 | | `forceConcise` | `bool` | Force concise responses. | `True` | 39 | | `disableTextFollowUps` | `bool` | Disable text follow-ups. | `True` | 40 | | `webpageUrls` | `List[str]` | List of webpage URLs. | `[]` | 41 | | `disableArtifact` | `bool` | Disable artifact flag. | `False` | 42 | | `toolOverrides` | `Dict[str, Any]` | Override tool settings. | `{}` | 43 | | `responseModelId` | `str` | Model ID for response metadata. | `"grok-3"` | 44 | 45 | > ⚙️ **toolOverrides:** 46 | > Some settings may not be recognized by the server or may be automatically overridden by the system. 47 | > For fine-tuning behavior, you can use the `toolOverrides` parameter, passing a dictionary with overrides. 48 | > For example, the official website sometimes automatically applies the following `toolOverrides` block: 49 | > ```python 50 | > toolOverrides = { 51 | > "imageGen": False, 52 | > "webSearch": True, 53 | > "xSearch": True, 54 | > "xMediaSearch": True, 55 | > "trendsSearch": True, 56 | > "xPostAnalyze": True 57 | > } 58 | > ``` 59 | > These options allow enabling or disabling various tools and model functions. 60 | 61 | 62 | > 💡 It is important to understand that these parameters are obtained by reverse engineering browser requests. And, perhaps, some of them may not yet have functionality, especially considering the freshness of the `Grok3` model 63 | 64 | > ❗ Descriptions of those parameters whose functionality could not be confirmed in testing are based on similar parameters in the official xAI API documentation. 65 | 66 | > 🛠️ You can contribute by simply experimenting with different options! 67 | -------------------------------------------------------------------------------- /grok3api/types/GeneratedImage.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from io import BytesIO 3 | from dataclasses import dataclass 4 | from typing import Optional, List 5 | 6 | from grok3api.logger import logger 7 | from grok3api import driver 8 | 9 | try: 10 | import aiofiles 11 | AIOFILES_AVAILABLE = True 12 | except ImportError: 13 | AIOFILES_AVAILABLE = False 14 | 15 | @dataclass 16 | class GeneratedImage: 17 | url: str 18 | _base_url: str = "https://assets.grok.com" 19 | cookies: Optional[List[dict]] = None 20 | 21 | def __post_init__(self): 22 | """ 23 | После инициализации проверяем driver.DRIVER и получаем куки для _base_url, 24 | если драйвер доступен. Иначе сохраняем cookies как None. 25 | """ 26 | if driver.web_driver is not None: 27 | # self.cookies = driver.web_driver.get_cookies() 28 | self.cookies = driver.web_driver._driver.get_cookies() 29 | else: 30 | self.cookies = None 31 | 32 | def download(self, timeout: int = driver.web_driver.TIMEOUT) -> Optional[BytesIO]: 33 | """Метод для загрузки изображения в память через браузер с таймаутом.""" 34 | try: 35 | image_data = self._fetch_image(timeout=timeout) 36 | if image_data is None: 37 | return None 38 | image_buffer = BytesIO(image_data) 39 | image_buffer.seek(0) 40 | return image_buffer 41 | except Exception as e: 42 | logger.error(f"При загрузке изображения (download): {e}") 43 | return None 44 | 45 | # async def async_download(self, timeout: int = 20) -> Optional[BytesIO]: 46 | # """Асинхронный метод для загрузки изображения в память с таймаутом. 47 | # 48 | # Args: 49 | # timeout (int): Таймаут в секундах (по умолчанию 20). 50 | # 51 | # Returns: 52 | # Optional[BytesIO]: Объект BytesIO с данными изображения или None при ошибке. 53 | # """ 54 | # try: 55 | # image_data = await asyncio.to_thread(self._fetch_image, timeout=timeout, proxy=driver.web_driver.def_proxy) 56 | # if image_data is None: 57 | # return None 58 | # image_buffer = BytesIO(image_data) 59 | # image_buffer.seek(0) 60 | # return image_buffer 61 | # except Exception as e: 62 | # logger.error(f"При загрузке изображения (download): {e}") 63 | # return None 64 | # 65 | # async def async_save_to(self, path: str, timeout: int = 10) -> None: 66 | # """Асинхронно скачивает изображение и сохраняет его в файл с таймаутом. 67 | # 68 | # Args: 69 | # path (str): Путь для сохранения файла. 70 | # timeout (int): Таймаут в секундах (по умолчанию 10). 71 | # """ 72 | # try: 73 | # logger.debug(f"Попытка сохранить изображение в файл: {path}") 74 | # image_data = await asyncio.to_thread(self._fetch_image, timeout=timeout, proxy=driver.web_driver.def_proxy) 75 | # image_data = BytesIO(image_data) 76 | # if image_data is not None: 77 | # if AIOFILES_AVAILABLE: 78 | # async with aiofiles.open(path, "wb") as f: 79 | # await f.write(image_data.getbuffer()) 80 | # else: 81 | # def write_file_sync(file_path: str, data: BytesIO): 82 | # with open(file_path, "wb") as file: 83 | # file.write(data.getbuffer()) 84 | # 85 | # await asyncio.to_thread(write_file_sync, path, image_data) 86 | # logger.debug(f"Изображение успешно сохранено в: {path}") 87 | # else: 88 | # logger.error("Изображение не было загружено, сохранение отменено.") 89 | # except Exception as e: 90 | # logger.error(f"В save_to: {e}") 91 | 92 | def download_to(self, path: str, timeout: int = driver.web_driver.TIMEOUT) -> None: 93 | """Скачивает изображение в файл через браузер с таймаутом.""" 94 | try: 95 | image_data = self._fetch_image(timeout=timeout) 96 | if image_data is not None: 97 | with open(path, "wb") as f: 98 | f.write(image_data) 99 | logger.debug(f"Изображение сохранено в: {path}") 100 | else: 101 | logger.debug("Изображение не загружено, сохранение отменено.") 102 | except Exception as e: 103 | logger.error(f"При загрузке в файл: {e}") 104 | 105 | def save_to(self, path: str, timeout: int = driver.web_driver.TIMEOUT) -> bool: 106 | """Скачивает изображение через download() и сохраняет его в файл с таймаутом.""" 107 | try: 108 | logger.debug(f"Попытка сохранить изображение в файл: {path}") 109 | image_data = self.download(timeout=timeout) 110 | if image_data is not None: 111 | with open(path, "wb") as f: 112 | f.write(image_data.getbuffer()) 113 | logger.debug(f"Изображение успешно сохранено в: {path}") 114 | return True 115 | else: 116 | logger.debug("Изображение не было загружено, сохранение отменено.") 117 | return False 118 | except Exception as e: 119 | logger.error(f"В save_to: {e}") 120 | return False 121 | 122 | def _fetch_image(self, timeout: int = driver.web_driver.TIMEOUT, proxy: Optional[str] = driver.web_driver.def_proxy) -> Optional[bytes]: 123 | """Приватная функция для загрузки изображения через браузер с таймаутом.""" 124 | if not self.cookies or len(self.cookies) == 0: 125 | logger.debug("Нет cookies для загрузки изображения.") 126 | return None 127 | 128 | image_url = self.url if self.url.startswith('/') else '/' + self.url 129 | full_url = self._base_url + image_url 130 | logger.debug(f"Полный URL для загрузки изображения: {full_url}, timeout: {timeout} сек") 131 | 132 | fetch_script = f""" 133 | console.log("Starting fetch with credentials: 'include'"); 134 | console.log("Cookies in browser before fetch:", document.cookie); 135 | 136 | const request = fetch('{full_url}', {{ 137 | method: 'GET' 138 | }}) 139 | .then(response => {{ 140 | console.log("Response status:", response.status); 141 | console.log("Response headers:", Array.from(response.headers.entries())); 142 | const contentType = response.headers.get('Content-Type'); 143 | if (!response.ok) {{ 144 | console.log("Request failed with status:", response.status); 145 | return 'Error: HTTP ' + response.status; 146 | }} 147 | if (!contentType || !contentType.startsWith('image/')) {{ 148 | return response.text().then(text => {{ 149 | console.log("Invalid MIME type detected:", contentType); 150 | console.log("Response content:", text); 151 | return 'Error: Invalid MIME type: ' + contentType + ', content: ' + text; 152 | }}); 153 | }} 154 | return response.arrayBuffer(); 155 | }}) 156 | .then(buffer => {{ 157 | console.log("Image data received, length:", buffer.byteLength); 158 | return Array.from(new Uint8Array(buffer)); 159 | }}) 160 | .catch(error => {{ 161 | console.log("Fetch error:", error.toString()); 162 | return 'Error: ' + error; 163 | }}); 164 | 165 | console.log("Fetch request sent, awaiting response..."); 166 | return request; 167 | """ 168 | driver.web_driver.init_driver(wait_loading=False) 169 | try: 170 | try: 171 | for cookie in self.cookies: 172 | if 'name' in cookie and 'value' in cookie: 173 | if 'domain' not in cookie or not cookie['domain']: 174 | cookie['domain'] = '.grok.com' 175 | driver.web_driver.add_cookie(cookie) 176 | else: 177 | logger.warning(f"Пропущена некорректная куки: {cookie}") 178 | logger.debug(f"Установлены куки: {self.cookies}") 179 | except Exception as e: 180 | logger.error(f"Ошибка при установке куки: {e}") 181 | return None 182 | 183 | driver.web_driver.get(full_url) 184 | response = driver.web_driver.execute_script(fetch_script) 185 | if response and 'This service is not available in your region' in response: 186 | driver.web_driver.set_proxy(proxy) 187 | driver.web_driver.get(full_url) 188 | response = driver.web_driver.execute_script(fetch_script) 189 | driver.web_driver.get(driver.web_driver.BASE_URL) 190 | except Exception as e: 191 | logger.error(f"Ошибка выполнения скрипта в браузере: {e}") 192 | return None 193 | 194 | if isinstance(response, str) and response.startswith('Error:'): 195 | logger.error(f"Ошибка при загрузке изображения: {response}") 196 | return None 197 | 198 | image_data = bytes(response) 199 | logger.debug("Изображение успешно загружено через браузер.") 200 | return image_data 201 | -------------------------------------------------------------------------------- /docs/Ru/askDoc.md: -------------------------------------------------------------------------------- 1 | # Описания метода `ask` 2 | 3 | 4 | ## 🚀 Отправляет запрос к API Grok и получает ответ. Есть асинхронный вариант `async_ask`. 5 | 6 | ### 📨 **Принимает:** 7 | - 📜`message`: Текст запроса для модели. 8 | - ⚙ `**kwargs`: Дополнительные параметры для настройки. 9 | 10 | ### 🎯 **Возвращает:** 11 | - `GrokResponse` – объект с ответом от API Grok. 12 | - **[Описание `GrokResponse`](GrokResponse.md)** 13 | 14 | 15 | ### Полный список параметров для `ask`: 16 | 17 | | Параметр | Тип | Описание | По умолчанию | 18 | |-----------------------------|-----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| 19 | | `message` | `str` | **Обязательный**. Текст запроса для Grok. | - | 20 | | `history_id` | `str` | Идентификатор в словаре историй (если используется авто сохранение истории) | `None` | 21 | | `new_conversation` | `bool` | Использовать ли url нового чата при отправке запроса в Grok (не касается встроенного класса History). | `False` | 22 | | `timeout` | `int` | Таймаут одного ожидания получения ответа (секунды). | `45` | 23 | | `temporary` | `bool` | Указывает, временный ли запрос или сессия. | `False` | 24 | | `modelName` | `str` | Название модели AI (например, "grok-3"). | `"grok-3"` | 25 | | `images` | `str_path` / `str_base64` / `BytesIO` / `List[str]` / `List[BytesIO]` | Или путь к изображению, или base64-кодированное изображение, или BytesIO (можно список любого из перечисленных типов) для отправки. Не должно быть использовано fileAttachments. | `None` | 26 | | `fileAttachments` | `List[str]` | Список файлов-вложений (ключи: `name`, `content`). | `[]` | 27 | | `imageAttachments` | `List[]` | Список изображений-вложений (ключи: `name`, `content`). | `[]` | 28 | | `customInstructions` | `str` | Дополнительные инструкции для модели. | `""` | 29 | | `deepsearch_preset` | `str` | Пресет для глубинного поиска. | `""` | 30 | | `disableSearch` | `bool` | Отключить поиск в ответах модели. | `False` | 31 | | `enableImageGeneration` | `bool` | Включить генерацию изображений. | `True` | 32 | | `enableImageStreaming` | `bool` | Включить потоковую передачу изображений. | `True` | 33 | | `enableSideBySide` | `bool` | Включить отображение информации бок о бок. | `True` | 34 | | `imageGenerationCount` | `int` | Количество генерируемых изображений. | `4` | 35 | | `isPreset` | `bool` | Указывает, предустановленное ли сообщение. | `False` | 36 | | `isReasoning` | `bool` | Включить режим рассуждений модели. | `False` | 37 | | `returnImageBytes` | `bool` | Возвращать изображения в виде байтов. | `False` | 38 | | `returnRawGrokInXaiRequest` | `bool` | Возвращать необработанный вывод модели. | `False` | 39 | | `sendFinalMetadata` | `bool` | Отправлять финальные метаданные вместе с запросом. | `True` | 40 | | `forceConcise` | `bool` | Принудительно включить краткий режим ответов. | `True` | 41 | | `disableTextFollowUps` | `bool` | Отключить последующие текстовые уточнения. | `True` | 42 | | `webpageUrls` | `List[str]` | Список URL веб-страниц для контекста. | `[]` | 43 | | `disableArtifact` | `bool` | Отключить артефакты. | `False` | 44 | | `toolOverrides` | `Dict[str, Any]` | Переопределение настроек инструментов. | `{}` | 45 | | `responseModelId` | `str` | ID модели для метаданных ответа. | `"grok-3"` | 46 | 47 | 48 | > ⚙️ **toolOverrides:** 49 | > Некоторые настройки могут не восприниматься сервером или автоматически перезаписываться системой. 50 | > Для тонкой настройки поведения можно использовать параметр `toolOverrides`, передавая в него словарь с переопределениями. 51 | > Например, на официальном сайте иногда автоматически подставляется такой блок toolOverrides: 52 | > ```python 53 | > toolOverrides = { 54 | > "imageGen": False, 55 | > "webSearch": True, 56 | > "xSearch": True, 57 | > "xMediaSearch": True, 58 | > "trendsSearch": True, 59 | > "xPostAnalyze": True 60 | > } 61 | > ``` 62 | > Эти опции позволяют включать или отключать различные инструменты и функции модели. 63 | 64 | 65 | > 💡 Важно понимать, что эти параметры получены путем реверс-инженеринга браузерных запросов. И, возможно, некоторые из них пока могут не иметь функционала, особенно учитывая, свежесть модели `Grok3` 66 | 67 | > ❗ Описание тех параметров, функционал которых не удалось подтвердить в тестировании, составлены на основе похожих параметров в официальной документации xAI API. 68 | 69 | > 🛠️ Вы можете внести свой вклад, просто экспериментируя с различными параметрами! -------------------------------------------------------------------------------- /docs/Ru/GrokResponse.md: -------------------------------------------------------------------------------- 1 | # Описание объекта `GrokResponse` 2 | 3 | ## 🚀 Обзор 4 | Объект `GrokResponse` возвращается методом `create` и содержит полную информацию о ответе от API Grok. Этот объект предоставляет как сам ответ модели (текст, изображения, вложения), так и метаданные, связанные с процессом генерации, включая состояние ответа, идентификаторы и дополнительные параметры. 5 | 6 | --- 7 | 8 | ### 🎯 **Что возвращает метод create** 9 | Метод `create` возвращает объект `GrokResponse`, который служит основным контейнером для всех данных, полученных от API Grok в ответ на запрос пользователя. Он объединяет текст ответа, сгенерированные изображения, вложения и информацию о состоянии обработки запроса. 10 | 11 | --- 12 | 13 | ### 📋 **Структура объекта GrokResponse** 14 | 15 | Объект `GrokResponse` включает следующие поля: 16 | 17 | | Поле | Тип | Описание | 18 | |--------------------------|-----------------------------|------------------------------------------------------------------------------------------| 19 | | `modelResponse` | `ModelResponse` | Вложенный объект с основным ответом модели (текст, изображения, вложения). | 20 | | `isThinking` | `bool` | Указывает, продолжает ли модель обработку ответа (`True` — в процессе). | 21 | | `isSoftStop` | `bool` | Указывает, был ли ответ остановлен по критерию (например, длина). | 22 | | `responseId` | `str` | Уникальный идентификатор ответа. | 23 | | `conversationId` | `Optional[str]` | Идентификатор чата, к которому относится данный ответ. | 24 | | `title` | `Optional[str]` | Заголовок чата, если был сгенерирован или обновлён (иначе `None`). | 25 | | `conversationCreateTime` | `Optional[str]` | Время создания чата (в формате строки ISO 8601) или `None`, если неизвестно. | 26 | | `conversationModifyTime` | `Optional[str]` | Время последнего изменения чата (в формате строки ISO 8601) или `None`, если неизвестно. | 27 | | `temporary` | `Optional[bool]` | Признак временного чата (`True` — временный, `False` — постоянный, `None` — неизвестно). | 28 | | `error` | `Optional[str]` | Сообщение об ошибке. `None` — если ошибка не произошла. | 29 | | `error_code` | `Optional[Union[int, str]]` | Код ошибки. `None` — если ошибка не произошла. `Unknown` — если ошибка без кода. | 30 | 31 | 32 | --- 33 | 34 | ### 📜 **Подробное описание полей** 35 | 36 | - **`modelResponse`** 37 | **Тип:** `ModelResponse` 38 | Основной вложенный объект, содержащий ответ модели. Включает текст (`message`), сгенерированные изображения (`generatedImages`), вложения и дополнительные метаданные. Для доступа к тексту используйте `modelResponse.message`. 39 | 40 | - **`isThinking`** 41 | **Тип:** `bool` 42 | Показывает, находится ли модель в процессе генерации ответа. Если `False`, ответ полностью готов. 43 | 44 | - **`isSoftStop`** 45 | **Тип:** `bool` 46 | Указывает, был ли процесс генерации прерван по критерию, например, из-за достижения максимальной длины текста. 47 | 48 | - **`responseId`** 49 | **Тип:** `str` 50 | Уникальный идентификатор ответа, который можно использовать для отслеживания или связи с запросом. 51 | 52 | - **`newTitle`** 53 | **Тип:** `Optional[str]` 54 | Опциональный заголовок, который может быть сгенерирован или обновлён в процессе обработки. Если заголовок не изменялся, значение равно `None`. 55 | 56 | --- 57 | 58 | ### 🌟 **Пример использования** 59 | 60 | ```python 61 | from grok3api.client import GrokClient 62 | 63 | 64 | def main(): 65 | cookies = "YOUR_COOKIES_FROM_BROWSER" 66 | 67 | # Создаём клиент 68 | client = GrokClient(cookies=cookies) 69 | 70 | # Отправляем запрос 71 | response = client.ask(message="Привет, Grok!") 72 | 73 | # Выводим текст ответа 74 | print(response.modelResponse.message) # "Здравствуйте! Чем могу помочь?" 75 | 76 | # Проверяем, завершён ли ответ 77 | print(response.isThinking) # False (ответ готов) 78 | 79 | # Выводим идентификатор ответа 80 | print(response.responseId) # "abc123XYZ" 81 | 82 | # Проверяем новый заголовок 83 | print(response.newTitle) # None или новый заголовок 84 | 85 | 86 | if __name__ == '__main__': 87 | main() 88 | ``` 89 | 90 | --- 91 | 92 | ### 🔗 **Связанные объекты** 93 | 94 | - **`ModelResponse`** 95 | Вложенный объект внутри `GrokResponse`, содержащий текст ответа, вложения (например, изображения или файлы) и метаданные. Подробности описаны ниже. 96 | 97 | - **`GeneratedImage`** 98 | Объект для работы со сгенерированными изображениями, доступный через `modelResponse.generatedImages`. Используется для загрузки и сохранения изображений. 99 | 100 | --- 101 | 102 | ### 📌 **Примечания** 103 | 104 | - **`GrokResponse` как контейнер** 105 | Этот объект объединяет всю информацию об ответе API и предоставляет удобный доступ к данным через свои поля. 106 | 107 | - **Доступ к тексту ответа** 108 | Используйте `response.modelResponse.message`, чтобы получить текст ответа. 109 | 110 | - **Работа с изображениями** 111 | Если в ответе есть изображения, они доступны через `response.modelResponse.generatedImages`. Каждое изображение — объект `GeneratedImage` с методами для загрузки и сохранения. 112 | 113 | --- 114 | 115 | ## 📋 **Дополнительно: Структура объекта ModelResponse** 116 | 117 | `ModelResponse` — ключевая часть `GrokResponse`, содержащая детализированный ответ модели. Вот обновлённая структура его полей: 118 | 119 | | Поле | Тип | Описание | 120 | |---------------------------|------------------------|----------------------------------------------------------------------------| 121 | | `responseId` | `str` | Уникальный идентификатор ответа. | 122 | | `message` | `str` | Текст ответа модели. | 123 | | `sender` | `str` | Отправитель сообщения (обычно "ASSISTANT"). | 124 | | `createTime` | `str` | Время создания ответа в формате ISO. | 125 | | `parentResponseId` | `str` | ID сообщения, на которое отвечает данный ответ. | 126 | | `manual` | `bool` | Указывает, создан ли ответ вручную (`False` — сгенерирован моделью). | 127 | | `partial` | `bool` | Указывает, является ли ответ неполным (`True` — ещё генерируется). | 128 | | `shared` | `bool` | Указывает, разделён ли ответ с другими (`True` — да, `False` — приватный). | 129 | | `query` | `str` | Оригинальный запрос пользователя. | 130 | | `queryType` | `str` | Тип запроса (для аналитики). | 131 | | `webSearchResults` | `List[Any]` | Результаты веб-поиска, использованные моделью. | 132 | | `xpostIds` | `List[Any]` | IDs X-постов, на которые ссылалась модель. | 133 | | `xposts` | `List[Any]` | X-посты, на которые ссылалась модель. | 134 | | `generatedImages` | `List[GeneratedImage]` | Список сгенерированных изображений. | 135 | | `imageAttachments` | `List[Any]` | Список вложений изображений. | 136 | | `fileAttachments` | `List[Any]` | Список вложений файлов. | 137 | | `cardAttachmentsJson` | `List[Any]` | JSON-данные для вложений типа "карточка". | 138 | | `fileUris` | `List[Any]` | URIs прикреплённых файлов. | 139 | | `fileAttachmentsMetadata` | `List[Any]` | Метаданные вложений файлов. | 140 | | `isControl` | `bool` | Указывает, является ли ответ системным (например, сообщение об ошибке). | 141 | | `steps` | `List[Any]` | Шаги или процесс рассуждений модели для генерации ответа. | 142 | | `mediaTypes` | `List[Any]` | Типы медиа в ответе (например, "image", "file"). | 143 | 144 | > 💡 Не все поля используются в каждом запросе. Например, `webSearchResults` или `steps` заполняются только при определённых условиях. 145 | 146 | --- 147 | 148 | ## 📋 **Дополнительно: Структура объекта GeneratedImage** 149 | 150 | Объект `GeneratedImage` используется для работы с изображениями, доступными через `modelResponse.generatedImages`: 151 | 152 | | Поле | Тип | Описание | 153 | |-------------|-------|-------------------------------------------------------------------| 154 | | `cookies` | `str` | Cookies для доступа к изображению (на случай перезапуска Chrome). | 155 | | `url` | `str` | Неполный URL изображения (`anon-users/...-generated_image.jpg`). | 156 | | `_base_url` | `str` | Базовый URL (по умолчанию "https://assets.grok.com"). | 157 | 158 | ### Методы `GeneratedImage` 159 | - **`download() -> Optional[BytesIO]`** 160 | Загружает изображение и возвращает его как объект `BytesIO`. 161 | 162 | - **`save_to(path: str) -> bool`** 163 | Сохраняет изображение в файл по указанному пути. 164 | 165 | 166 | #### Пример работы с изображением: 167 | 168 | ```python 169 | from grok3api.client import GrokClient 170 | 171 | 172 | def main(): 173 | cookies = "YOUR_COOKIES_FROM_BROWSER" 174 | 175 | # Создаём клиент 176 | client = GrokClient(cookies=cookies) 177 | 178 | # Отправляем запрос для создания изображения 179 | response = client.ask(message="Создай изображение корабля") 180 | 181 | # Проверяем, есть ли сгенерированные изображения, и сохраняем первое 182 | if response.modelResponse.generatedImages: 183 | image = response.modelResponse.generatedImages[0] 184 | image.save_to("ship.jpg") # Сохраняет изображение как ship.jpg 185 | print("Изображение корабля сохранено как ship.jpg") 186 | else: 187 | print("Изображения не были сгенерированы.") 188 | 189 | 190 | if __name__ == '__main__': 191 | main() 192 | ``` 193 | 194 | --- 195 | 196 | ### 🛠️ **Советы по использованию** 197 | 198 | - **Получение текста:** Используйте `response.modelResponse.message` для быстрого доступа к тексту. 199 | - **Проверка статуса:** Поле `isThinking` показывает, завершён ли ответ (скоро будет добавлена возможность получения ответа по частям). 200 | - **Работа с изображениями:** Используйте методы `download()` и `save_to()` для загрузки и сохранения картинок. 201 | - **Эксперименты:** Пробуйте разные параметры в методе `create`, чтобы раскрыть дополнительные возможности. -------------------------------------------------------------------------------- /docs/En/GrokResponse.md: -------------------------------------------------------------------------------- 1 | # Description of the `GrokResponse` Object 2 | 3 | ## 🚀 Overview 4 | The `GrokResponse` object is returned by the `create` method and contains complete information about the response from the Grok API. This object provides both the model's response itself (text, images, attachments) and metadata related to the generation process, including the response state, identifiers, and additional parameters. 5 | 6 | --- 7 | 8 | ### 🎯 **What the create method returns** 9 | The `create` method returns the `GrokResponse` object, which serves as the main container for all data received from the Grok API in response to a user's request. It combines the response text, generated images, attachments, and information about the request processing state. 10 | 11 | --- 12 | 13 | ### 📋 **Structure of the GrokResponse object** 14 | The `GrokResponse` object includes the following fields: 15 | 16 | 17 | | Field | Type | Description | 18 | |--------------------------|-----------------------------|---------------------------------------------------------------------------------------------------------------| 19 | | `modelResponse` | `ModelResponse` | Nested object with the main model response (text, images, attachments). | 20 | | `isThinking` | `bool` | Indicates whether the model is still processing the response (`True` — in progress). | 21 | | `isSoftStop` | `bool` | Indicates whether the response was stopped by a criterion (e.g., length limit). | 22 | | `responseId` | `str` | Unique identifier of the response. | 23 | | `conversationId` | `Optional[str]` | Identifier of the conversation to which the response belongs. | 24 | | `title` | `Optional[str]` | Conversation title if generated or updated (otherwise `None`). | 25 | | `conversationCreateTime` | `Optional[str]` | Conversation creation time (ISO 8601 format) or `None` if unknown. | 26 | | `conversationModifyTime` | `Optional[str]` | Last modification time of the conversation (ISO 8601 format) or `None` if unknown. | 27 | | `temporary` | `Optional[bool]` | Indicates whether the conversation is temporary (`True` — temporary, `False` — persistent, `None` — unknown). | 28 | | `error` | `Optional[str]` | Error message. `None` if no error occurred. | 29 | | `error_code` | `Optional[Union[int, str]]` | Error code. `None` if no error occurred. `Unknown` if an error occurred without a code. | 30 | 31 | --- 32 | 33 | ### 📜 **Detailed description of fields** 34 | 35 | - **`modelResponse`** 36 | **Type:** `ModelResponse` 37 | The main nested object containing the model's response. Includes the text (`message`), generated images (`generatedImages`), attachments, and additional metadata. To access the text, use `modelResponse.message`. 38 | 39 | - **`isThinking`** 40 | **Type:** `bool` 41 | Indicates whether the model is still in the process of generating the response. If `False`, the response is fully ready. 42 | 43 | - **`isSoftStop`** 44 | **Type:** `bool` 45 | Indicates whether the generation process was interrupted based on a criterion, such as reaching the maximum text length. 46 | 47 | - **`responseId`** 48 | **Type:** `str` 49 | A unique identifier for the response, which can be used for tracking or linking with the request. 50 | 51 | - **`newTitle`** 52 | **Type:** `Optional[str]` 53 | An optional title that may be generated or updated during processing. If the title was not changed, the value is `None`. 54 | 55 | --- 56 | 57 | ### 🌟 **Example usage** 58 | 59 | ```python 60 | from grok3api.client import GrokClient 61 | 62 | 63 | def main(): 64 | cookies = "YOUR_COOKIES_FROM_BROWSER" 65 | 66 | # Create a client 67 | client = GrokClient(cookies=cookies) 68 | 69 | # Send a request 70 | response = client.ask(message="Hello, Grok!") 71 | 72 | # Print the response text 73 | print(response.modelResponse.message) # "Hello! How can I help you?" 74 | 75 | # Check if the response is complete 76 | print(response.isThinking) # False (response is ready) 77 | 78 | # Print the response identifier 79 | print(response.responseId) # "abc123XYZ" 80 | 81 | # Check the new title 82 | print(response.newTitle) # None or the new title 83 | 84 | 85 | if __name__ == '__main__': 86 | main() 87 | ``` 88 | 89 | --- 90 | 91 | ### 🔗 **Related objects** 92 | 93 | - **`ModelResponse`** 94 | A nested object within `GrokResponse` that contains the response text, attachments (e.g., images or files), and metadata. Details are described below. 95 | 96 | - **`GeneratedImage`** 97 | An object for working with generated images, accessible via `modelResponse.generatedImages`. It is used for downloading and saving images. 98 | 99 | --- 100 | 101 | ### 📌 **Notes** 102 | 103 | - **`GrokResponse` as a container** 104 | This object combines all information about the API response and provides convenient access to the data through its fields. 105 | 106 | - **Access to response text** 107 | Use `response.modelResponse.message` to get the response text. 108 | 109 | - **Working with images** 110 | If there are images in the response, they are available via `response.modelResponse.generatedImages`. Each image is a `GeneratedImage` object with methods for downloading and saving. 111 | 112 | --- 113 | 114 | ## 📋 **Additional: Structure of the ModelResponse object** 115 | 116 | `ModelResponse` is a key part of `GrokResponse`, containing the detailed response from the model. Here is the updated structure of its fields: 117 | 118 | | Field | Type | Description | 119 | |---------------------------|------------------------|-----------------------------------------------------------------------------------------| 120 | | `responseId` | `str` | Unique identifier of the response. | 121 | | `message` | `str` | The text of the model's response. | 122 | | `sender` | `str` | The sender of the message (usually "ASSISTANT"). | 123 | | `createTime` | `str` | The creation time of the response in ISO format. | 124 | | `parentResponseId` | `str` | The ID of the message to which this response is replying. | 125 | | `manual` | `bool` | Indicates whether the response was created manually (`False` — generated by the model). | 126 | | `partial` | `bool` | Indicates whether the response is incomplete (`True` — still being generated). | 127 | | `shared` | `bool` | Indicates whether the response is shared with others (`True` — yes, `False` — private). | 128 | | `query` | `str` | The original user query. | 129 | | `queryType` | `str` | The type of query (for analytics). | 130 | | `webSearchResults` | `List[Any]` | Web search results used by the model. | 131 | | `xpostIds` | `List[Any]` | IDs of X-posts referenced by the model. | 132 | | `xposts` | `List[Any]` | X-posts referenced by the model. | 133 | | `generatedImages` | `List[GeneratedImage]` | List of generated images. | 134 | | `imageAttachments` | `List[Any]` | List of image attachments. | 135 | | `fileAttachments` | `List[Any]` | List of file attachments. | 136 | | `cardAttachmentsJson` | `List[Any]` | JSON data for "card" type attachments. | 137 | | `fileUris` | `List[Any]` | URIs of attached files. | 138 | | `fileAttachmentsMetadata` | `List[Any]` | Metadata of file attachments. | 139 | | `isControl` | `bool` | Indicates whether the response is a system message (e.g., error message). | 140 | | `steps` | `List[Any]` | Steps or reasoning process of the model for generating the response. | 141 | | `mediaTypes` | `List[Any]` | Types of media in the response (e.g., "image", "file"). | 142 | 143 | > 💡 Not all fields are used in every request. For example, `webSearchResults` or `steps` are only filled under certain conditions. 144 | 145 | --- 146 | 147 | ## 📋 **Additional: Structure of the GeneratedImage object** 148 | 149 | The `GeneratedImage` object is used for working with images available through `modelResponse.generatedImages`: 150 | 151 | | Field | Type | Description | 152 | |-------------|-------|------------------------------------------------------------------| 153 | | `cookies` | `str` | Cookies for accessing the image (in case of restarting Chrome). | 154 | | `url` | `str` | Partial URL of the image (`anon-users/...-generated_image.jpg`). | 155 | | `_base_url` | `str` | Base URL (default is "https://assets.grok.com"). | 156 | 157 | ### Methods of `GeneratedImage` 158 | - **`download() -> Optional[BytesIO]`** 159 | Downloads the image and returns it as a `BytesIO` object. 160 | 161 | - **`save_to(path: str) -> bool`** 162 | Saves the image to a file at the specified path. 163 | 164 | #### Example of working with an image: 165 | 166 | ```python 167 | from grok3api.client import GrokClient 168 | 169 | 170 | def main(): 171 | cookies = "YOUR_COOKIES_FROM_BROWSER" 172 | 173 | # Create a client 174 | client = GrokClient(cookies=cookies) 175 | 176 | # Send a request to create an image 177 | response = client.ask(message="Create an image of a ship") 178 | 179 | # Check if there are generated images and save the first one 180 | if response.modelResponse.generatedImages: 181 | image = response.modelResponse.generatedImages[0] 182 | image.save_to("ship.jpg") # Saves the image as ship.jpg 183 | print("The image of the ship has been saved as ship.jpg") 184 | else: 185 | print("No images were generated.") 186 | 187 | 188 | if __name__ == '__main__': 189 | main() 190 | ``` 191 | 192 | --- 193 | 194 | ### 🛠️ **Tips for use** 195 | 196 | - **Getting the text:** Use `response.modelResponse.message` for quick access to the text. 197 | - **Checking the status:** The `isThinking` field shows whether the response is complete (soon, the ability to receive the response in parts will be added). 198 | - **Working with images:** Use the `download()` and `save_to()` methods to download and save images. 199 | - **Experiments:** Try different parameters in the `create` method to unlock additional features. 200 | -------------------------------------------------------------------------------- /grok3api/driver.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | import re 4 | import string 5 | import time 6 | from typing import Optional 7 | import os 8 | import shutil 9 | import subprocess 10 | import atexit 11 | import signal 12 | import sys 13 | 14 | from selenium.webdriver.common.keys import Keys 15 | from selenium.webdriver import ActionChains 16 | from selenium.webdriver.chrome.options import Options 17 | from selenium.webdriver.chrome.webdriver import WebDriver as ChromeWebDriver 18 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 19 | import undetected_chromedriver as uc 20 | from selenium.webdriver.common.by import By 21 | from selenium.webdriver.support.ui import WebDriverWait 22 | from selenium.webdriver.support import expected_conditions as ec 23 | from selenium.common.exceptions import SessionNotCreatedException, TimeoutException 24 | 25 | from grok3api.logger import logger 26 | 27 | class WebDriverSingleton: 28 | """Синглтон для управления ChromeDriver.""" 29 | _instance = None 30 | _driver: Optional[ChromeWebDriver] = None 31 | TIMEOUT = 360 32 | 33 | USE_XVFB = True 34 | xvfb_display: Optional[int] = None 35 | 36 | BASE_URL = "https://grok.com/" 37 | CHROME_VERSION = None 38 | WAS_FATAL = False 39 | def_proxy = "socks4://98.178.72.21:10919" 40 | 41 | execute_script = None 42 | add_cookie = None 43 | get_cookies = None 44 | get = None 45 | 46 | need_proxy: bool = False 47 | max_proxy_tries = 1 48 | proxy_try = 0 49 | proxy: Optional[str] = None 50 | 51 | def __new__(cls): 52 | if cls._instance is None: 53 | cls._instance = super(WebDriverSingleton, cls).__new__(cls) 54 | return cls._instance 55 | 56 | def __init__(self): 57 | self._hide_unnecessary_logs() 58 | self._patch_chrome_del() 59 | atexit.register(self.close_driver) 60 | signal.signal(signal.SIGINT, self._signal_handler) 61 | 62 | def _hide_unnecessary_logs(self): 63 | """Подавляет ненужные логи.""" 64 | try: 65 | uc_logger = logging.getLogger("undetected_chromedriver") 66 | for handler in uc_logger.handlers[:]: 67 | uc_logger.removeHandler(handler) 68 | uc_logger.setLevel(logging.CRITICAL) 69 | 70 | selenium_logger = logging.getLogger("selenium") 71 | for handler in selenium_logger.handlers[:]: 72 | selenium_logger.removeHandler(handler) 73 | selenium_logger.setLevel(logging.CRITICAL) 74 | 75 | urllib3_con_logger = logging.getLogger("urllib3.connectionpool") 76 | for handler in urllib3_con_logger.handlers[:]: 77 | urllib3_con_logger.removeHandler(handler) 78 | urllib3_con_logger.setLevel(logging.CRITICAL) 79 | 80 | 81 | logging.getLogger("selenium.webdriver").setLevel(logging.CRITICAL) 82 | logging.getLogger("selenium.webdriver.remote.remote_connection").setLevel(logging.CRITICAL) 83 | except KeyboardInterrupt: 84 | raise 85 | except Exception as e: 86 | logging.debug(f"Ошибка при подавлении логов (_hide_unnecessary_logs): {e}") 87 | 88 | def _patch_chrome_del(self): 89 | """Патчит метод __del__ для uc.Chrome.""" 90 | def safe_del(self): 91 | try: 92 | try: 93 | if hasattr(self, 'service') and self.service.process: 94 | self.service.process.kill() 95 | logger.debug("Процесс сервиса ChromeDriver успешно завершен.") 96 | except Exception as e: 97 | logger.debug(f"Ошибка при завершении процесса сервиса: {e}") 98 | try: 99 | self.quit() 100 | logger.debug("ChromeDriver успешно закрыт через quit().") 101 | except Exception as e: 102 | logger.debug(f"uc.Chrome.__del__: при вызове quit(): {e}") 103 | except Exception as e: 104 | logger.error(f"uc.Chrome.__del__: {e}") 105 | try: 106 | uc.Chrome.__del__ = safe_del 107 | except: 108 | pass 109 | 110 | def _is_driver_alive(self, driver): 111 | """Проверяет, живой ли драйвер.""" 112 | try: 113 | driver.title 114 | return True 115 | except: 116 | return False 117 | 118 | def _setup_driver(self, driver, wait_loading: bool, timeout: int): 119 | """Настраивает драйвер: минимизирует, загружает базовый URL и ждет поле ввода.""" 120 | self._minimize() 121 | 122 | driver.get(self.BASE_URL) 123 | patch_fetch_for_statsig(driver) 124 | 125 | page = driver.page_source 126 | if not page is None and isinstance(page, str) and 'This service is not available in your region' in page: 127 | if self.proxy_try > self.max_proxy_tries: 128 | raise ValueError("Cant bypass region block") 129 | 130 | self.need_proxy = True 131 | self.close_driver() 132 | self.init_driver(wait_loading=wait_loading, proxy=self.def_proxy) 133 | self.proxy_try += 1 134 | 135 | 136 | if wait_loading: 137 | logger.debug("Ждем загрузки страницы с неявным ожиданием...") 138 | try: 139 | WebDriverWait(driver, timeout).until( 140 | ec.presence_of_element_located((By.CSS_SELECTOR, "div.relative.z-10 textarea")) 141 | ) 142 | time.sleep(2) 143 | # statsig_id = driver.execute_script(""" 144 | # for (let key in localStorage) { 145 | # if (key.startsWith('statsig.stable_id')) { 146 | # return localStorage.getItem(key); 147 | # } 148 | # } 149 | # return null; 150 | # """) 151 | # print(f"statsig.stable_id: {statsig_id}") 152 | self.proxy_try = 0 153 | logger.debug("Поле ввода найдено.") 154 | except Exception: 155 | logger.debug("Поле ввода не найдено") 156 | 157 | def init_driver(self, wait_loading: bool = True, use_xvfb: bool = True, timeout: Optional[int] = None, proxy: Optional[str] = None): 158 | """Запускает ChromeDriver и проверяет/устанавливает базовый URL с тремя попытками.""" 159 | driver_timeout = timeout if timeout is not None else self.TIMEOUT 160 | self.TIMEOUT = driver_timeout 161 | if proxy is None: 162 | if self.need_proxy: 163 | proxy = self.def_proxy 164 | else: 165 | self.proxy = proxy 166 | 167 | self.USE_XVFB = use_xvfb 168 | attempts = 0 169 | max_attempts = 3 170 | 171 | def _create_driver(): 172 | chrome_options = Options() 173 | chrome_options.add_argument("--no-sandbox") 174 | chrome_options.add_argument("--incognito") 175 | chrome_options.add_argument("--disable-blink-features=AutomationControlled") 176 | chrome_options.add_argument("--disable-gpu") 177 | chrome_options.add_argument("--disable-dev-shm-usage") 178 | #chrome_options.add_argument("--auto-open-devtools-for-tabs") 179 | 180 | caps = DesiredCapabilities.CHROME 181 | caps['goog:loggingPrefs'] = {'browser': 'ALL'} 182 | 183 | if proxy: 184 | logger.debug(f"Добавляем прокси в опции: {proxy}") 185 | chrome_options.add_argument(f"--proxy-server={proxy}") 186 | 187 | new_driver = uc.Chrome(options=chrome_options, headless=False, use_subprocess=True, version_main=self.CHROME_VERSION, desired_capabilities=caps) 188 | new_driver.set_script_timeout(driver_timeout) 189 | return new_driver 190 | 191 | while attempts < max_attempts: 192 | try: 193 | if self.USE_XVFB: 194 | self._safe_start_xvfb() 195 | 196 | if self._driver and self._is_driver_alive(self._driver): 197 | self._minimize() 198 | current_url = self._driver.current_url 199 | if current_url != self.BASE_URL: 200 | logger.debug(f"Текущий URL ({current_url}) не совпадает с базовым ({self.BASE_URL}), переходим...") 201 | self._driver.get(self.BASE_URL) 202 | if wait_loading: 203 | logger.debug("Ждем загрузки страницы с неявным ожиданием...") 204 | try: 205 | WebDriverWait(self._driver, driver_timeout).until( 206 | ec.presence_of_element_located((By.CSS_SELECTOR, "div.relative.z-10 textarea")) 207 | ) 208 | time.sleep(2) 209 | wait_loading = False 210 | logger.debug("Поле ввода найдено.") 211 | except Exception: 212 | logger.error("Поле ввода не найдено.") 213 | self.WAS_FATAL = False 214 | logger.debug("Драйвер живой, все ок.") 215 | 216 | self.execute_script = self._driver.execute_script 217 | self.add_cookie = self._driver.add_cookie 218 | self.get_cookies = self._driver.get_cookies 219 | self.get = self._driver.get 220 | 221 | return 222 | 223 | logger.debug(f"Попытка {attempts + 1}: создаем новый драйвер...") 224 | 225 | self.close_driver() 226 | self._driver = _create_driver() 227 | self._setup_driver(self._driver, wait_loading, driver_timeout) 228 | self.WAS_FATAL = False 229 | 230 | logger.debug("Браузер запущен") 231 | 232 | self.execute_script = self._driver.execute_script 233 | self.add_cookie = self._driver.add_cookie 234 | self.get_cookies = self._driver.get_cookies 235 | self.get = self._driver.get 236 | 237 | return 238 | 239 | except SessionNotCreatedException as e: 240 | self.close_driver() 241 | error_message = str(e) 242 | match = re.search(r"Current browser version is (\d+)", error_message) 243 | if match: 244 | current_version = int(match.group(1)) 245 | else: 246 | current_version = self._get_chrome_version() 247 | self.CHROME_VERSION = current_version 248 | logger.debug(f"Несовместимость браузера и драйвера, пробуем переустановить драйвер для Chrome {self.CHROME_VERSION}...") 249 | self._driver = _create_driver() 250 | self._setup_driver(self._driver, wait_loading, driver_timeout) 251 | logger.debug(f"Удалось установить версию драйвера на {self.CHROME_VERSION}.") 252 | self.WAS_FATAL = False 253 | 254 | self.execute_script = self._driver.execute_script 255 | self.add_cookie = self._driver.add_cookie 256 | return 257 | 258 | except Exception as e: 259 | logger.error(f"В попытке {attempts + 1}: {e}") 260 | attempts += 1 261 | self.close_driver() 262 | if attempts == max_attempts: 263 | logger.fatal(f"Все {max_attempts} попыток неуспешны: {e}") 264 | self.WAS_FATAL = True 265 | raise e 266 | logger.debug("Ждем 1 секунду перед следующей попыткой...") 267 | time.sleep(1) 268 | 269 | 270 | def restart_session(self): 271 | """Перезапускает сессию, очищая куки, localStorage, sessionStorage и перезагружая страницу.""" 272 | try: 273 | self._driver.delete_all_cookies() 274 | self._driver.execute_script("localStorage.clear();") 275 | self._driver.execute_script("sessionStorage.clear();") 276 | self._driver.get(self.BASE_URL) 277 | patch_fetch_for_statsig(self._driver) 278 | WebDriverWait(self._driver, self.TIMEOUT).until( 279 | ec.presence_of_element_located((By.CSS_SELECTOR, "div.relative.z-10 textarea")) 280 | ) 281 | time.sleep(2) 282 | logger.debug("Страница загружена, сессия обновлена.") 283 | except Exception as e: 284 | logger.debug(f"Ошибка при перезапуске сессии: {e}") 285 | 286 | def set_cookies(self, cookies_input): 287 | """Устанавливает куки в драйвере.""" 288 | if cookies_input is None: 289 | return 290 | current_url = self._driver.current_url 291 | if not current_url.startswith("http"): 292 | raise Exception("Перед установкой куки нужно сначала открыть сайт в драйвере!") 293 | 294 | if isinstance(cookies_input, str): 295 | cookie_string = cookies_input.strip().rstrip(";") 296 | cookies = cookie_string.split("; ") 297 | for cookie in cookies: 298 | if "=" not in cookie: 299 | continue 300 | name, value = cookie.split("=", 1) 301 | self._driver.add_cookie({ 302 | "name": name, 303 | "value": value, 304 | "path": "/" 305 | }) 306 | elif isinstance(cookies_input, dict): 307 | if "name" in cookies_input and "value" in cookies_input: 308 | cookie = cookies_input.copy() 309 | cookie.setdefault("path", "/") 310 | self._driver.add_cookie(cookie) 311 | else: 312 | for name, value in cookies_input.items(): 313 | self._driver.add_cookie({ 314 | "name": name, 315 | "value": value, 316 | "path": "/" 317 | }) 318 | elif isinstance(cookies_input, list): 319 | for cookie in cookies_input: 320 | if isinstance(cookie, dict) and "name" in cookie and "value" in cookie: 321 | cookie = cookie.copy() 322 | cookie.setdefault("path", "/") 323 | self._driver.add_cookie(cookie) 324 | else: 325 | raise ValueError("Каждый словарь в списке должен содержать 'name' и 'value'") 326 | else: 327 | raise TypeError("cookies_input должен быть строкой, словарем или списком словарей") 328 | 329 | def close_driver(self): 330 | """Закрывает драйвер.""" 331 | if self._driver: 332 | self._driver.quit() 333 | logger.debug("Браузер закрыт.") 334 | self._driver = None 335 | 336 | def set_proxy(self, proxy: str): 337 | """Меняет прокси в текущей сессии драйвера.""" 338 | self.close_driver() 339 | self.init_driver(use_xvfb=self.USE_XVFB, timeout=self.TIMEOUT, proxy=proxy) 340 | 341 | def _minimize(self): 342 | """Минимизирует окно браузера.""" 343 | try: 344 | self._driver.minimize_window() 345 | except Exception: 346 | pass 347 | 348 | def _safe_start_xvfb(self): 349 | """Запускает Xvfb на уникальном DISPLAY, и сохраняет его в переменную окружения.""" 350 | if not sys.platform.startswith("linux"): 351 | return 352 | 353 | if shutil.which("Xvfb") is None: 354 | logger.error("Xvfb не установлен! Установите его командой: sudo apt install xvfb") 355 | raise RuntimeError("Xvfb отсутствует") 356 | 357 | if self.xvfb_display is None: 358 | display_number = 99 359 | while True: 360 | result = subprocess.run(["pgrep", "-f", f"Xvfb :{display_number}"], capture_output=True, text=True) 361 | if not result.stdout.strip(): 362 | break 363 | display_number += 1 364 | self.xvfb_display = display_number 365 | 366 | display_var = f":{self.xvfb_display}" 367 | os.environ["DISPLAY"] = display_var 368 | 369 | result = subprocess.run(["pgrep", "-f", f"Xvfb {display_var}"], capture_output=True, text=True) 370 | if result.stdout.strip(): 371 | logger.debug(f"Xvfb уже запущен на дисплее {display_var}.") 372 | return 373 | 374 | logger.debug(f"Запускаем Xvfb на дисплее {display_var}...") 375 | subprocess.Popen(["Xvfb", display_var, "-screen", "0", "1024x768x24"], 376 | stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 377 | 378 | for _ in range(10): 379 | time.sleep(1) 380 | result = subprocess.run(["pgrep", "-f", f"Xvfb {display_var}"], capture_output=True, text=True) 381 | if result.stdout.strip(): 382 | logger.debug(f"Xvfb успешно запущен на дисплее {display_var}.") 383 | return 384 | 385 | raise RuntimeError(f"Xvfb не запустился на дисплее {display_var} за 10 секунд!") 386 | 387 | def _get_chrome_version(self): 388 | """Определяет текущую версию Chrome.""" 389 | if "win" in sys.platform.lower(): 390 | try: 391 | import winreg 392 | reg_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe" 393 | with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path) as key: 394 | chrome_path, _ = winreg.QueryValueEx(key, "") 395 | 396 | output = subprocess.check_output([chrome_path, "--version"], shell=True, text=True).strip() 397 | version = re.search(r"(\d+)\.", output).group(1) 398 | return int(version) 399 | except Exception as e: 400 | logger.debug(f"Не удалось найти версию Chrome через реестр: {e}") 401 | 402 | chrome_paths = [ 403 | r"C:\Program Files\Google\Chrome\Application\chrome.exe", 404 | r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" 405 | ] 406 | 407 | for path in chrome_paths: 408 | if os.path.exists(path): 409 | try: 410 | output = subprocess.check_output([path, "--version"], shell=True, text=True).strip() 411 | version = re.search(r"(\d+)\.", output).group(1) 412 | return int(version) 413 | except Exception as e: 414 | logger.debug(f"Ошибка при получении версии Chrome по пути {path}: {e}") 415 | continue 416 | 417 | logger.error("Не удалось найти Chrome или его версию на Windows.") 418 | return None 419 | else: 420 | cmd = r'google-chrome --version' 421 | try: 422 | output = subprocess.check_output(cmd, shell=True, text=True).strip() 423 | version = re.search(r"(\d+)\.", output).group(1) 424 | return int(version) 425 | except Exception as e: 426 | logger.error(f"Ошибка при получении версии Chrome: {e}") 427 | return None 428 | 429 | def _signal_handler(self, sig, frame): 430 | """Обрабатывает сигналы для корректного завершения.""" 431 | logger.debug("Остановка...") 432 | self.close_driver() 433 | sys.exit(0) 434 | 435 | def get_statsig(self, restart_session=False, try_index = 0) -> Optional[str]: 436 | if try_index > 3: 437 | return None 438 | statsig_id: Optional[str] = None 439 | try: 440 | statsig_id = self._update_statsig(restart_session) 441 | except Exception as e: 442 | logger.error(f"In get_statsig: {e}") 443 | finally: 444 | return statsig_id if statsig_id else self._update_statsig(True) 445 | 446 | def _initiate_answer(self): 447 | try: 448 | # news_button = WebDriverWait(self._driver, self.TIMEOUT).until( 449 | # ec.element_to_be_clickable((By.CSS_SELECTOR, "button.inline-flex:has(svg.lucide-newspaper)")) 450 | # ) 451 | # logger.debug("Кнопка новостей найдена") 452 | # self._driver.execute_script("arguments[0].click();", news_button) 453 | # logger.debug("Кнопка нажата, ждём ответа") 454 | textarea = WebDriverWait(self._driver, self.TIMEOUT).until( 455 | ec.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'relative')]//textarea")) 456 | ) 457 | textarea.send_keys(random.choice(string.ascii_lowercase)) 458 | textarea.send_keys(Keys.ENTER) 459 | except Exception as e: 460 | logger.error(f"In _initiate_answer: {e}") 461 | 462 | def _update_statsig(self, restart_session=False) -> Optional[str]: 463 | if restart_session: 464 | self.restart_session() 465 | current_url = self._driver.current_url 466 | 467 | if current_url != self.BASE_URL: 468 | logger.debug(f"Текущий URL {current_url} не совпадает с BASE_URL {self.BASE_URL}. Переход на BASE_URL.") 469 | self._driver.get(self.BASE_URL) 470 | patch_fetch_for_statsig(self._driver) 471 | logger.debug(f"Перешел на {self.BASE_URL}") 472 | 473 | self._initiate_answer() 474 | 475 | try: 476 | is_overlay_active = self._driver.execute_script(""" 477 | const elements = document.querySelectorAll("p"); 478 | for (const el of elements) { 479 | if (el.textContent.includes("Making sure you're human")) { 480 | const style = window.getComputedStyle(el); 481 | if (style.visibility !== 'hidden' && style.display !== 'none') { 482 | return true; 483 | } 484 | } 485 | } 486 | return false; 487 | """) 488 | 489 | if is_overlay_active: 490 | logger.debug("Обнаружен overlay с капчей — блокируем процесс.") 491 | return None 492 | 493 | 494 | WebDriverWait(self._driver, min(self.TIMEOUT, 20)).until( 495 | ec.any_of( 496 | ec.presence_of_element_located((By.CSS_SELECTOR, "div.message-bubble p[dir='auto']")), 497 | ec.presence_of_element_located((By.CSS_SELECTOR, "div.w-full.max-w-\\48rem\\]")), 498 | ec.presence_of_element_located((By.XPATH, "//p[contains(text(), \"Making sure you're human...\")]")) 499 | ) 500 | ) 501 | 502 | if self._driver.find_elements(By.CSS_SELECTOR, "div.w-full.max-w-\\48rem\\]"): 503 | logger.debug("Ошибка подлинности") 504 | return None 505 | 506 | captcha_elements = self._driver.find_elements(By.XPATH, 507 | "//p[contains(text(), \"Making sure you're human...\")]") 508 | if captcha_elements: 509 | logger.debug("Появилась капча 'Making sure you're human...'") 510 | return None 511 | 512 | logger.debug("Элемент ответа появился") 513 | statsig_id = self._driver.execute_script("return window.__xStatsigId;") 514 | logger.debug(f"Получен x-statsig-id: {statsig_id}") 515 | return statsig_id 516 | 517 | except TimeoutException: 518 | logger.debug("Ни ответа, ни ошибки, возвращаю None") 519 | return None 520 | except Exception as e: 521 | logger.debug(f"В _update_statsig: {e}") 522 | return None 523 | 524 | def del_captcha(self, timeout = 5): 525 | try: 526 | captcha_wrapper = WebDriverWait(self._driver, timeout).until( 527 | ec.presence_of_element_located((By.CSS_SELECTOR, "div.main-wrapper")) 528 | ) 529 | self._driver.execute_script("arguments[0].remove();", captcha_wrapper) 530 | return True 531 | except TimeoutException: 532 | return True 533 | except Exception as e: 534 | logger.debug(f"В del_captcha: {e}") 535 | return False 536 | 537 | 538 | 539 | def patch_fetch_for_statsig(driver): 540 | result = driver.execute_script(""" 541 | if (window.__fetchPatched) { 542 | return "fetch уже патчен"; 543 | } 544 | 545 | window.__fetchPatched = false; 546 | const originalFetch = window.fetch; 547 | window.__xStatsigId = null; 548 | 549 | window.fetch = async function(...args) { 550 | console.log("➡️ Перехваченный fetch вызов с аргументами:", args); 551 | 552 | const response = await originalFetch.apply(this, args); 553 | 554 | try { 555 | const req = args[0]; 556 | const opts = args[1] || {}; 557 | const url = typeof req === 'string' ? req : req.url; 558 | const headers = opts.headers || {}; 559 | 560 | const targetUrl = "https://grok.com/rest/app-chat/conversations/new"; 561 | 562 | if (url === targetUrl) { 563 | let id = null; 564 | if (headers["x-statsig-id"]) { 565 | id = headers["x-statsig-id"]; 566 | } else if (typeof opts.headers?.get === "function") { 567 | id = opts.headers.get("x-statsig-id"); 568 | } 569 | 570 | if (id) { 571 | window.__xStatsigId = id; 572 | console.log("✅ Сохранили x-statsig-id:", id); 573 | } else { 574 | console.warn("⚠️ x-statsig-id не найден в заголовках"); 575 | } 576 | } else { 577 | console.log("ℹ️ Пропущен fetch, не совпадает с целевым URL:", url); 578 | } 579 | } catch (e) { 580 | console.warn("❌ Ошибка при извлечении x-statsig-id:", e); 581 | } 582 | 583 | return response; 584 | }; 585 | 586 | window.__fetchPatched = true; 587 | return "fetch успешно патчен"; 588 | """) 589 | # print(result) 590 | # 591 | # driver.execute_script(""" 592 | # fetch('https://grok.com/rest/app-chat/conversations/new', { 593 | # headers: {'x-statsig-id': 'test123'} 594 | # }); 595 | # """) 596 | # 597 | # import time 598 | # time.sleep(1) 599 | # 600 | # statsig_id = driver.execute_script("return window.__xStatsigId;") 601 | # print("Captured x-statsig-id:", statsig_id) 602 | 603 | 604 | web_driver = WebDriverSingleton() -------------------------------------------------------------------------------- /grok3api/client.py: -------------------------------------------------------------------------------- 1 | import contextvars 2 | import functools 3 | import os 4 | from asyncio import events 5 | from typing import Optional, List, Union, Dict, Any, Tuple 6 | import base64 7 | import json 8 | from io import BytesIO 9 | 10 | from grok3api.history import History, SenderType 11 | from grok3api import driver 12 | from grok3api.logger import logger 13 | from grok3api.types.GrokResponse import GrokResponse 14 | 15 | 16 | 17 | class GrokClient: 18 | """ 19 | Client for working with Grok. 20 | 21 | :param use_xvfb: Flag to use Xvfb. Defaults to True. Applicable only on Linux. 22 | :param proxy: (str) Proxy server URL, used only in cases of regional blocking. 23 | :param history_msg_count: Number of messages to keep in history (default is `0` — history saving is disabled). 24 | :param history_path: Path to the history file in JSON format. Default is "chat_histories.json". 25 | :param history_as_json: Whether to send history to Grok in JSON format (for history_msg_count > 0). Defaults to True. 26 | :param history_auto_save: Automatically overwrite history file after each message. Defaults to True. 27 | :param always_new_conversation: (bool) Whether to use the new chat creation URL when sending a request to Grok. 28 | :param conversation_id: (str) Grok chat ID. Use this to continue a conversation from where it left off. Must be used together with response_id. 29 | :param response_id: (str) Grok response ID in the conversation_id chat. Use this to continue a conversation from where it left off. Must be used together with conversation_id. 30 | :param timeout: Maximum time for client initialization. Default is 120 seconds. 31 | """ 32 | 33 | NEW_CHAT_URL = "https://grok.com/rest/app-chat/conversations/new" 34 | CONVERSATION_URL = "https://grok.com/rest/app-chat/conversations/" # + {conversationId}/responses/ 35 | max_tries: int = 5 36 | 37 | def __init__(self, 38 | cookies: Union[Union[str, List[str]], Union[dict, List[dict]]] = None, 39 | use_xvfb: bool = True, 40 | proxy: Optional[str] = None, 41 | history_msg_count: int = 0, 42 | history_path: str = "chat_histories.json", 43 | history_as_json: bool = True, 44 | history_auto_save: bool = True, 45 | always_new_conversation: bool = True, 46 | conversation_id: Optional[str] = None, 47 | response_id: Optional[str] = None, 48 | enable_artifact_files: bool = False, 49 | main_system_prompt: Optional[str] = None, 50 | timeout: int = driver.web_driver.TIMEOUT): 51 | try: 52 | if (conversation_id is None) != (response_id is None): 53 | raise ValueError( 54 | "If you want to use server history, you must provide both conversation_id and response_id.") 55 | 56 | self.cookies = cookies 57 | self.proxy = proxy 58 | self.use_xvfb: bool = use_xvfb 59 | self.history = History(history_msg_count=history_msg_count, 60 | history_path=history_path, 61 | history_as_json=history_as_json, 62 | main_system_prompt=main_system_prompt) 63 | self.history_auto_save: bool = history_auto_save 64 | self.proxy_index = 0 65 | self.enable_artifact_files = enable_artifact_files 66 | self.timeout: int = timeout 67 | 68 | self.always_new_conversation: bool = always_new_conversation 69 | self.conversationId: Optional[str] = conversation_id 70 | self.parentResponseId: Optional[str] = response_id 71 | self._statsig_id: Optional[str] = None 72 | 73 | driver.web_driver.init_driver(use_xvfb=self.use_xvfb, timeout=timeout, proxy=self.proxy) 74 | 75 | self._statsig_id = driver.web_driver.get_statsig() 76 | except Exception as e: 77 | logger.error(f"В GrokClient.__init__: {e}") 78 | raise e 79 | 80 | def _send_request(self, 81 | payload, 82 | headers, 83 | timeout=driver.web_driver.TIMEOUT): 84 | try: 85 | """Отправляем запрос через браузер с таймаутом.""" 86 | 87 | 88 | if not self._statsig_id: 89 | self._statsig_id = driver.web_driver.get_statsig() 90 | 91 | headers.update({ 92 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", 93 | "Accept": "*/*", 94 | "Accept-Encoding": "gzip, deflate, br, zstd", 95 | "Accept-Language": "ru-RU,ru;q=0.9", 96 | "Content-Type": "application/json", 97 | "Origin": "https://grok.com", 98 | "Referer": "https://grok.com/", 99 | "Sec-Ch-Ua": '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"', 100 | "Sec-Ch-Ua-Mobile": "?0", 101 | "Sec-Ch-Ua-Platform": '"Windows"', 102 | "Sec-Fetch-Dest": "empty", 103 | "Sec-Fetch-Mode": "cors", 104 | "Sec-Fetch-Site": "same-origin", 105 | "x-statsig-id": self._statsig_id, 106 | }) 107 | 108 | target_url = self.CONVERSATION_URL + self.conversationId + "/responses" if self.conversationId else self.NEW_CHAT_URL 109 | 110 | fetch_script = f""" 111 | const controller = new AbortController(); 112 | const signal = controller.signal; 113 | setTimeout(() => controller.abort(), {timeout * 1000}); 114 | 115 | const payload = {json.dumps(payload)}; 116 | return fetch('{target_url}', {{ 117 | method: 'POST', 118 | headers: {json.dumps(headers)}, 119 | body: JSON.stringify(payload), 120 | credentials: 'include', 121 | signal: signal 122 | }}) 123 | .then(response => {{ 124 | if (!response.ok) {{ 125 | return response.text().then(text => 'Error: HTTP ' + response.status + ' - ' + text); 126 | }} 127 | return response.text(); 128 | }}) 129 | .catch(error => {{ 130 | if (error.name === 'AbortError') {{ 131 | return 'TimeoutError'; 132 | }} 133 | return 'Error: ' + error; 134 | }}); 135 | """ 136 | response = driver.web_driver.execute_script(fetch_script) 137 | # print(response) 138 | 139 | if isinstance(response, str) and response.startswith('Error:'): 140 | error_data = self.handle_str_error(response) 141 | if isinstance(error_data, dict): 142 | return error_data 143 | 144 | if response and 'This service is not available in your region' in response: 145 | return 'This service is not available in your region' 146 | 147 | final_dict = {} 148 | conversation_info = {} 149 | new_title = None 150 | 151 | for line in response.splitlines(): 152 | try: 153 | 154 | parsed = json.loads(line) 155 | 156 | if "modelResponse" in parsed.get("result", {}): 157 | parsed["result"]["response"] = {"modelResponse": parsed["result"].pop("modelResponse")} 158 | 159 | if "conversation" in parsed.get("result", {}): 160 | conversation_info = parsed["result"]["conversation"] 161 | 162 | if "title" in parsed.get("result", {}): 163 | new_title = parsed["result"]["title"].get("newTitle") 164 | 165 | if "modelResponse" in parsed.get("result", {}).get("response", {}): 166 | final_dict = parsed 167 | elif "modelResponse" in parsed.get("result", {}): 168 | parsed["result"]["response"] = conversation_info 169 | except (json.JSONDecodeError, KeyError): 170 | continue 171 | 172 | if final_dict: 173 | model_response = final_dict["result"]["response"]["modelResponse"] 174 | final_dict["result"]["response"] = {"modelResponse": model_response} 175 | final_dict["result"]["response"]["conversationId"] = conversation_info.get("conversationId") 176 | final_dict["result"]["response"]["title"] = conversation_info.get("title") 177 | final_dict["result"]["response"]["createTime"] = conversation_info.get("createTime") 178 | final_dict["result"]["response"]["modifyTime"] = conversation_info.get("modifyTime") 179 | final_dict["result"]["response"]["temporary"] = conversation_info.get("temporary") 180 | final_dict["result"]["response"]["newTitle"] = new_title 181 | 182 | if not self.always_new_conversation and model_response.get("responseId"): 183 | self.conversationId = self.conversationId or conversation_info.get("conversationId") 184 | self.parentResponseId = model_response.get("responseId") if self.conversationId else None 185 | 186 | logger.debug(f"Получили ответ: {final_dict}") 187 | return final_dict 188 | except Exception as e: 189 | logger.error(f"В _send_request: {e}") 190 | return {} 191 | 192 | IMAGE_SIGNATURES = { 193 | b'\xff\xd8\xff': ("jpg", "image/jpeg"), 194 | b'\x89PNG\r\n\x1a\n': ("png", "image/png"), 195 | b'GIF89a': ("gif", "image/gif") 196 | } 197 | 198 | def _is_base64_image(self, s: str) -> bool: 199 | try: 200 | decoded = base64.b64decode(s, validate=True) 201 | return any(decoded.startswith(sig) for sig in self.IMAGE_SIGNATURES) 202 | except Exception: 203 | return False 204 | 205 | def _get_extension_and_mime_from_header(self, data: bytes) -> Tuple[str, str]: 206 | for sig, (ext, mime) in self.IMAGE_SIGNATURES.items(): 207 | if data.startswith(sig): 208 | return ext, mime 209 | return "jpg", "image/jpeg" 210 | 211 | def _upload_image(self, 212 | file_input: Union[str, BytesIO], 213 | file_extension: str = "jpg", 214 | file_mime_type: str = None) -> str: 215 | """ 216 | Uploads an image to the server from a file path or BytesIO and returns the fileMetadataId from the response. 217 | 218 | Args: 219 | file_input (Union[str, BytesIO]): File path or a BytesIO object containing the file content. 220 | file_extension (str): File extension without the dot (e.g., "jpg", "png"). Defaults to "jpg". 221 | file_mime_type (str): MIME type of the file. If None, it is determined automatically. 222 | 223 | Returns: 224 | str: fileMetadataId from the server response. 225 | 226 | Raises: 227 | ValueError: If the input data is invalid or the response does not contain fileMetadataId. 228 | """ 229 | 230 | if isinstance(file_input, str): 231 | if os.path.exists(file_input): 232 | with open(file_input, "rb") as f: 233 | file_content = f.read() 234 | elif self._is_base64_image(file_input): 235 | file_content = base64.b64decode(file_input) 236 | else: 237 | raise ValueError("The string is neither a valid file path nor a valid base64 image string") 238 | elif isinstance(file_input, BytesIO): 239 | file_content = file_input.getvalue() 240 | else: 241 | raise ValueError("file_input must be a file path, a base64 string, or a BytesIO object") 242 | 243 | if file_extension is None or file_mime_type is None: 244 | ext, mime = self._get_extension_and_mime_from_header(file_content) 245 | file_extension = file_extension or ext 246 | file_mime_type = file_mime_type or mime 247 | 248 | file_content_b64 = base64.b64encode(file_content).decode("utf-8") 249 | file_name_base = file_content_b64[:10].replace("/", "_").replace("+", "_") 250 | file_name = f"{file_name_base}.{file_extension}" 251 | 252 | b64_str_js_safe = json.dumps(file_content_b64) 253 | file_name_js_safe = json.dumps(file_name) 254 | file_mime_type_js_safe = json.dumps(file_mime_type) 255 | 256 | fetch_script = f""" 257 | return fetch('https://grok.com/rest/app-chat/upload-file', {{ 258 | method: 'POST', 259 | headers: {{ 260 | 'Content-Type': 'application/json', 261 | 'Accept': '*/*', 262 | 'User-Agent': 'Mozilla/5.0', 263 | 'Origin': 'https://grok.com', 264 | 'Referer': 'https://grok.com/' 265 | }}, 266 | body: JSON.stringify({{ 267 | fileName: {file_name_js_safe}, 268 | fileMimeType: {file_mime_type_js_safe}, 269 | content: {b64_str_js_safe} 270 | }}), 271 | credentials: 'include' 272 | }}) 273 | .then(response => {{ 274 | if (!response.ok) {{ 275 | return response.text().then(text => 'Error: HTTP ' + response.status + ' - ' + text); 276 | }} 277 | return response.json(); 278 | }}) 279 | .catch(error => 'Error: ' + error); 280 | """ 281 | 282 | response = driver.web_driver.execute_script(fetch_script) 283 | 284 | capcha = "Just a moment" in response 285 | if (isinstance(response, str) and response.startswith('Error:')) or capcha: 286 | if 'Too many requests' in response or 'Bad credentials' in response or capcha: 287 | driver.web_driver.restart_session() 288 | response = driver.web_driver.execute_script(fetch_script) 289 | if isinstance(response, str) and response.startswith('Error:'): 290 | raise ValueError(response) 291 | else: 292 | raise ValueError(response) 293 | 294 | if not isinstance(response, dict) or "fileMetadataId" not in response: 295 | raise ValueError("Server response does not contain fileMetadataId") 296 | 297 | return response["fileMetadataId"] 298 | 299 | def _clean_conversation(self, payload: dict, history_id: str, message: str): 300 | if payload and "parentResponseId" in payload: 301 | del payload["parentResponseId"] 302 | payload["message"] = self._messages_with_possible_history(history_id, message) 303 | self.conversationId = None 304 | self.parentResponseId = None 305 | 306 | def _messages_with_possible_history(self, history_id: str, message: str) -> str: 307 | if (self.history.history_msg_count < 1 and self.history.main_system_prompt is None 308 | and history_id not in self.history._system_prompts): 309 | message_payload = message 310 | elif self.parentResponseId and self.conversationId: 311 | message_payload = message 312 | else: 313 | message_payload = self.history.get_history(history_id) + '\n' + message 314 | return message_payload 315 | 316 | 317 | def send_message(self, 318 | message: str, 319 | history_id: Optional[str] = None, 320 | **kwargs: Any) -> GrokResponse: 321 | """Устаревший метод отправки сообщения. Please, use ask method.""" 322 | logger.warning("Please, use GrokClient.ask method instead GrokClient.send_message") 323 | return self.ask(message=message, 324 | history_id=history_id, 325 | **kwargs) 326 | 327 | async def async_ask(self, 328 | message: str, 329 | history_id: Optional[str] = None, 330 | new_conversation: Optional[bool] = None, 331 | timeout: Optional[int] = None, 332 | temporary: bool = False, 333 | modelName: str = "grok-3", 334 | images: Union[Optional[List[Union[str, BytesIO]]], str, BytesIO] = None, 335 | fileAttachments: Optional[List[str]] = None, 336 | imageAttachments: Optional[List] = None, 337 | customInstructions: str = "", 338 | deepsearch_preset: str = "", 339 | disableSearch: bool = False, 340 | enableImageGeneration: bool = True, 341 | enableImageStreaming: bool = True, 342 | enableSideBySide: bool = True, 343 | imageGenerationCount: int = 2, 344 | isPreset: bool = False, 345 | isReasoning: bool = False, 346 | returnImageBytes: bool = False, 347 | returnRawGrokInXaiRequest: bool = False, 348 | sendFinalMetadata: bool = True, 349 | toolOverrides: Optional[Dict[str, Any]] = None, 350 | forceConcise: bool = True, 351 | disableTextFollowUps: bool = True, 352 | webpageUrls: Optional[List[str]] = None, 353 | disableArtifact: bool = False, 354 | responseModelId: str = "grok-3" 355 | ) -> GrokResponse: 356 | """ 357 | Asynchronous wrapper for the ask method. 358 | Sends a request to the Grok API with a single message and additional parameters. 359 | 360 | Args: 361 | message (str): The user message to send to the API. 362 | history_id (Optional[str]): Identifier to specify which chat history to use. 363 | new_conversation (Optional[bool]): Whether to use the new chat URL when sending the request to Grok (does not apply to the built-in History class). 364 | timeout (Optional[int]): Timeout in seconds to wait for a response. 365 | temporary (bool): Indicates if the session or request is temporary. 366 | modelName (str): The AI model name for processing the request. 367 | images (str / BytesIO / List[str / BytesIO]): Either a path to an image, a base64-encoded image, or BytesIO object (or a list of any of these types) to send. Should not be used with fileAttachments. 368 | fileAttachments (Optional[List[str]]): List of file attachments. 369 | imageAttachments (Optional[List]): List of image attachments. 370 | customInstructions (str): Additional instructions or context for the model. 371 | deepsearch_preset (str): Preset for deep search. 372 | disableSearch (bool): Disable the model’s search functionality. 373 | enableImageGeneration (bool): Enable image generation in the response. 374 | enableImageStreaming (bool): Enable streaming of images. 375 | enableSideBySide (bool): Enable side-by-side display of information. 376 | imageGenerationCount (int): Number of images to generate. 377 | isPreset (bool): Indicates if the message is a preset. 378 | isReasoning (bool): Enable reasoning mode in the model’s response. 379 | returnImageBytes (bool): Return image data as bytes. 380 | returnRawGrokInXaiRequest (bool): Return raw output from the model. 381 | sendFinalMetadata (bool): Send final metadata with the request. 382 | toolOverrides (Optional[Dict[str, Any]]): Dictionary to override tool settings. 383 | forceConcise (bool): Whether to force concise responses. 384 | disableTextFollowUps (bool): Disable text follow-ups. 385 | webpageUrls (Optional[List[str]]): List of webpage URLs. 386 | disableArtifact (bool): Disable artifact flag. 387 | responseModelId (str): Model ID for the response metadata. 388 | 389 | Returns: 390 | GrokResponse: The response from the Grok API as an object. 391 | """ 392 | try: 393 | return await _to_thread(self.ask, 394 | message=message, 395 | history_id=history_id, 396 | new_conversation=new_conversation, 397 | timeout=timeout, 398 | temporary=temporary, 399 | modelName=modelName, 400 | images=images, 401 | fileAttachments=fileAttachments, 402 | imageAttachments=imageAttachments, 403 | customInstructions=customInstructions, 404 | deepsearch_preset=deepsearch_preset, 405 | disableSearch=disableSearch, 406 | enableImageGeneration=enableImageGeneration, 407 | enableImageStreaming=enableImageStreaming, 408 | enableSideBySide=enableSideBySide, 409 | imageGenerationCount=imageGenerationCount, 410 | isPreset=isPreset, 411 | isReasoning=isReasoning, 412 | returnImageBytes=returnImageBytes, 413 | returnRawGrokInXaiRequest=returnRawGrokInXaiRequest, 414 | sendFinalMetadata=sendFinalMetadata, 415 | toolOverrides=toolOverrides, 416 | forceConcise=forceConcise, 417 | disableTextFollowUps=disableTextFollowUps, 418 | webpageUrls=webpageUrls, 419 | disableArtifact=disableArtifact, 420 | responseModelId=responseModelId) 421 | except Exception as e: 422 | logger.error(f"In async_ask: {e}") 423 | return GrokResponse({}, self.enable_artifact_files) 424 | 425 | def ask(self, 426 | message: str, 427 | history_id: Optional[str] = None, 428 | new_conversation: Optional[bool] = None, 429 | timeout: Optional[int] = None, 430 | temporary: bool = False, 431 | modelName: str = "grok-3", 432 | images: Union[Optional[List[Union[str, BytesIO]]], str, BytesIO] = None, 433 | fileAttachments: Optional[List[str]] = None, 434 | imageAttachments: Optional[List] = None, 435 | customInstructions: str = "", 436 | deepsearch_preset: str = "", 437 | disableSearch: bool = False, 438 | enableImageGeneration: bool = True, 439 | enableImageStreaming: bool = True, 440 | enableSideBySide: bool = True, 441 | imageGenerationCount: int = 2, 442 | isPreset: bool = False, 443 | isReasoning: bool = False, 444 | returnImageBytes: bool = False, 445 | returnRawGrokInXaiRequest: bool = False, 446 | sendFinalMetadata: bool = True, 447 | toolOverrides: Optional[Dict[str, Any]] = None, 448 | forceConcise: bool = True, 449 | disableTextFollowUps: bool = True, 450 | webpageUrls: Optional[List[str]] = None, 451 | disableArtifact: bool = False, 452 | responseModelId: str = "grok-3", 453 | ) -> GrokResponse: 454 | """ 455 | Sends a request to the Grok API with a single message and additional parameters. 456 | 457 | Args: 458 | message (str): The user message to send to the API. 459 | history_id (Optional[str]): Identifier to specify which chat history to use. 460 | new_conversation (Optional[bool]): Whether to use the new chat URL when sending the request to Grok. 461 | timeout (Optional[int]): Timeout in seconds to wait for a response. 462 | temporary (bool): Indicates if the session or request is temporary. 463 | modelName (str): The AI model name for processing the request. 464 | images (str / BytesIO / List[str / BytesIO]): Image(s) to send. 465 | fileAttachments (Optional[List[str]]): List of file attachments. 466 | imageAttachments (Optional[List]): List of image attachments. 467 | customInstructions (str): Additional instructions for the model. 468 | deepsearch_preset (str): Preset for deep search. 469 | disableSearch (bool): Disable the model’s search functionality. 470 | enableImageGeneration (bool): Enable image generation in the response. 471 | enableImageStreaming (bool): Enable streaming of images. 472 | enableSideBySide (bool): Enable side-by-side display. 473 | imageGenerationCount (int): Number of images to generate. 474 | isPreset (bool): Indicates if the message is a preset. 475 | isReasoning (bool): Enable reasoning mode. 476 | returnImageBytes (bool): Return image data as bytes. 477 | returnRawGrokInXaiRequest (bool): Return raw model output. 478 | sendFinalMetadata (bool): Send final metadata with the request. 479 | toolOverrides (Optional[Dict[str, Any]]): Dictionary to override tool settings. 480 | forceConcise (bool): Whether to force concise responses. 481 | disableTextFollowUps (bool): Disable text follow-ups. 482 | webpageUrls (Optional[List[str]]): List of webpage URLs. 483 | disableArtifact (bool): Disable artifact flag. 484 | responseModelId (str): Model ID for the response metadata. 485 | 486 | Returns: 487 | GrokResponse: The response from the Grok API. 488 | """ 489 | 490 | if timeout is None: 491 | timeout = self.timeout 492 | 493 | 494 | if images is not None and fileAttachments is not None: 495 | raise ValueError("'images' and 'fileAttachments' cannot be used together") 496 | last_error_data = {} 497 | try: 498 | 499 | base_headers = { 500 | "Content-Type": "application/json", 501 | "User-Agent": ( 502 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " 503 | "(KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 OPR/119.0.0.0" 504 | ), 505 | "Accept": "*/*", 506 | "Accept-Encoding": "gzip, deflate, br, zstd", 507 | "Accept-Language": "ru", 508 | "Origin": "https://grok.com", 509 | "Referer": "https://grok.com/", 510 | "Sec-Fetch-Dest": "empty", 511 | "Sec-Fetch-Mode": "cors", 512 | "Sec-Fetch-Site": "same-origin", 513 | "Sec-CH-UA": '"Chromium";v="134", "Not:A-Brand";v="24", "Opera";v="119"', 514 | "Sec-CH-UA-Mobile": "?0", 515 | "Sec-CH-UA-Platform": '"Windows"', 516 | "Priority": "u=1, i", 517 | } 518 | 519 | headers = base_headers.copy() 520 | 521 | if images: 522 | fileAttachments = [] 523 | if isinstance(images, list): 524 | for image in images: 525 | fileAttachments.append(self._upload_image(image)) 526 | else: 527 | fileAttachments.append(self._upload_image(images)) 528 | 529 | 530 | message_payload = self._messages_with_possible_history(history_id, message) 531 | 532 | payload = { 533 | "temporary": temporary, 534 | "modelName": modelName, 535 | "message": message_payload, 536 | "fileAttachments": fileAttachments if fileAttachments is not None else [], 537 | "imageAttachments": imageAttachments if imageAttachments is not None else [], 538 | "disableSearch": disableSearch, 539 | "enableImageGeneration": enableImageGeneration, 540 | "returnImageBytes": returnImageBytes, 541 | "returnRawGrokInXaiRequest": returnRawGrokInXaiRequest, 542 | "enableImageStreaming": enableImageStreaming, 543 | "imageGenerationCount": imageGenerationCount, 544 | "forceConcise": forceConcise, 545 | "toolOverrides": toolOverrides if toolOverrides is not None else {}, 546 | "enableSideBySide": enableSideBySide, 547 | "sendFinalMetadata": sendFinalMetadata, 548 | "isPreset": isPreset, 549 | "isReasoning": isReasoning, 550 | "disableTextFollowUps": disableTextFollowUps, 551 | "customInstructions": customInstructions, 552 | "deepsearch preset": deepsearch_preset, 553 | 554 | "webpageUrls": webpageUrls if webpageUrls is not None else [], 555 | "disableArtifact": disableArtifact or not self.enable_artifact_files, 556 | "responseMetadata": { 557 | "requestModelDetails": { 558 | "modelId": responseModelId 559 | } 560 | } 561 | } 562 | 563 | if self.parentResponseId: 564 | payload["parentResponseId"] = self.parentResponseId 565 | 566 | logger.debug(f"Grok payload: {payload}") 567 | if new_conversation: 568 | self._clean_conversation(payload, history_id, message) 569 | 570 | statsig_try_index = 0 571 | statsig_try_max = 2 572 | safe_try_max = 4 573 | safe_try_index = 0 574 | try_index = 0 575 | response = "" 576 | use_cookies: bool = self.cookies is not None 577 | 578 | is_list_cookies = isinstance(self.cookies, list) 579 | 580 | while try_index < self.max_tries: 581 | logger.debug( 582 | f"Попытка {try_index + 1} из {self.max_tries}" + (" (Without cookies)" if not use_cookies else "")) 583 | cookies_used = 0 584 | 585 | while cookies_used < (len(self.cookies) if is_list_cookies else 1) or not use_cookies: 586 | if use_cookies: 587 | current_cookies = self.cookies[0] if is_list_cookies else self.cookies 588 | driver.web_driver.set_cookies(current_cookies) 589 | if images: 590 | fileAttachments = [] 591 | if isinstance(images, list): 592 | for image in images: 593 | fileAttachments.append(self._upload_image(image)) 594 | else: 595 | fileAttachments.append(self._upload_image(images)) 596 | payload["fileAttachments"] = fileAttachments if fileAttachments is not None else [] 597 | 598 | logger.debug( 599 | f"Отправляем запрос (cookie[{cookies_used}]): headers={headers}, payload={payload}, timeout={timeout} секунд") 600 | 601 | if new_conversation: 602 | self._clean_conversation(payload, history_id, message) 603 | 604 | if safe_try_index > safe_try_max: 605 | return GrokResponse(last_error_data, self.enable_artifact_files) 606 | safe_try_index += 1 607 | response = self._send_request(payload, headers, timeout) 608 | logger.debug(f"Ответ Grok: {response}") 609 | 610 | if response == {} and try_index != 0: 611 | try_index += 1 612 | driver.web_driver.close_driver() 613 | driver.web_driver.init_driver() 614 | 615 | self._clean_conversation(payload, history_id, message) 616 | 617 | continue 618 | 619 | if isinstance(response, dict) and response: 620 | last_error_data = response 621 | str_response = str(response) 622 | if 'Too many requests' in str_response or 'credentials' in str_response: 623 | self._clean_conversation(payload, history_id, message) 624 | cookies_used += 1 625 | 626 | if not is_list_cookies or cookies_used >= len(self.cookies) - 1: 627 | self._clean_conversation(payload, history_id, message) 628 | driver.web_driver.restart_session() 629 | use_cookies = False 630 | if images: 631 | fileAttachments = [] 632 | if isinstance(images, list): 633 | for image in images: 634 | fileAttachments.append(self._upload_image(image)) 635 | else: 636 | fileAttachments.append(self._upload_image(images)) 637 | payload["fileAttachments"] = fileAttachments if fileAttachments is not None else [] 638 | continue 639 | if is_list_cookies and len(self.cookies) > 1: 640 | self._clean_conversation(payload, history_id, message) 641 | self.cookies.append(self.cookies.pop(0)) 642 | continue 643 | 644 | elif 'This service is not available in your region' in str_response: 645 | return GrokResponse(last_error_data, self.enable_artifact_files) 646 | 647 | elif 'a padding to disable MSIE and Chrome friendly error page' in str_response or "Request rejected by anti-bot rules." in str_response: 648 | if not self.always_new_conversation: 649 | last_error_data["error"] = "Can not bypass x-statsig-id protection. Try `always_new_conversation = True` to bypass x-statsig-id protection" 650 | return GrokResponse(last_error_data, self.enable_artifact_files) 651 | 652 | if statsig_try_index < statsig_try_max: 653 | statsig_try_index += 1 654 | self._statsig_id = driver.web_driver.get_statsig(restart_session=True) 655 | continue 656 | 657 | last_error_data["error"] = "Can not bypass x-statsig-id protection" 658 | return GrokResponse(last_error_data, self.enable_artifact_files) 659 | 660 | elif 'Just a moment' in str_response or '403' in str_response: 661 | # driver.web_driver.close_driver() 662 | # driver.web_driver.init_driver() 663 | driver.web_driver.restart_session() 664 | self._clean_conversation(payload, history_id, message) 665 | break 666 | else: 667 | response = GrokResponse(response, self.enable_artifact_files) 668 | assistant_message = response.modelResponse.message 669 | 670 | if self.history.history_msg_count > 0: 671 | self.history.add_message(history_id, SenderType.ASSISTANT, assistant_message) 672 | if self.history_auto_save: 673 | self.history.to_file() 674 | 675 | return response 676 | else: 677 | break 678 | 679 | if is_list_cookies and cookies_used >= len(self.cookies): 680 | break 681 | 682 | try_index += 1 683 | 684 | if try_index == self.max_tries - 1: 685 | self._clean_conversation(payload, history_id, message) 686 | 687 | driver.web_driver.close_driver() 688 | driver.web_driver.init_driver() 689 | 690 | self._clean_conversation(payload, history_id, message) 691 | driver.web_driver.restart_session() 692 | 693 | logger.debug(f"(In ask) Bad response: {response}") 694 | driver.web_driver.restart_session() 695 | self._clean_conversation(payload, history_id, message) 696 | 697 | if not last_error_data: 698 | last_error_data = self.handle_str_error(response) 699 | 700 | except Exception as e: 701 | logger.debug(f"In ask: {e}") 702 | if not last_error_data: 703 | last_error_data = self.handle_str_error(str(e)) 704 | finally: 705 | if self.history.history_msg_count > 0: 706 | self.history.add_message(history_id, SenderType.ASSISTANT, message) 707 | if self.history_auto_save: 708 | self.history.to_file() 709 | return GrokResponse(last_error_data, self.enable_artifact_files) 710 | 711 | def handle_str_error(self, response_str): 712 | try: 713 | json_str = response_str.split(" - ", 1)[1] 714 | response = json.loads(json_str) 715 | 716 | if isinstance(response, dict): 717 | # {"error": {...}} 718 | if 'error' in response: 719 | error = response['error'] 720 | error_code = error.get('code', 'Unknown') 721 | error_message = error.get('message') or response_str 722 | error_details = error.get('details') if isinstance(error.get('details'), list) else [] 723 | # {"code": ..., "message": ..., "details": ...} 724 | elif 'message' in response: 725 | error_code = response.get('code', 'Unknown') 726 | error_message = response.get('message') or response_str 727 | error_details = response.get('details') if isinstance(response.get('details'), list) else [] 728 | else: 729 | raise ValueError("Unsupported error format") 730 | 731 | return { 732 | "error_code": error_code, 733 | "error": error_message, 734 | "details": error_details 735 | } 736 | 737 | except Exception: 738 | pass 739 | 740 | return { 741 | "error_code": "Unknown", 742 | "error": response_str, 743 | "details": [] 744 | } 745 | 746 | async def _to_thread(func, /, *args, **kwargs): 747 | 748 | loop = events.get_running_loop() 749 | ctx = contextvars.copy_context() 750 | func_call = functools.partial(ctx.run, func, *args, **kwargs) 751 | return await loop.run_in_executor(None, func_call) --------------------------------------------------------------------------------