├── 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 | 
10 | 
11 |
12 | 
13 | 
14 | 
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 |
109 |
110 | 🐹 Or, for instance, if you request "**A gopher on Elbrus**":
111 |
112 |
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 | 
7 | 
8 |
9 | 
10 | 
11 | 
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)
--------------------------------------------------------------------------------