/poe-api

A reverse engineered Python API wrapper for Quora's Poe, which provides free access to ChatGPT, GPT-4, and Claude.

Primary LanguagePythonGNU General Public License v3.0GPL-3.0

Python Poe API

PyPi Version

This is a reverse engineered API wrapper for Quora's Poe, which allows you free access to OpenAI's ChatGPT and GPT-4, as well as Anthropic's Claude.

Table of Contents:

Table of contents generated with markdown-toc.

Features:

  • Log in with token
  • Proxy requests + websocket
  • Download bot list
  • Send messages
  • Stream bot responses
  • Clear conversation context
  • Download conversation history
  • Delete messages
  • Purge an entire conversation
  • Create custom bots
  • Edit your custom bots
  • Use pre-existing third party bots

Installation:

You can install this library by running the following command:

pip3 install poe-api

Documentation:

Examples can be found in the /examples directory. To run these examples, pass in your token as a command-line argument.

python3 examples/temporary_message.py "TOKEN_HERE"

Finding Your Token:

Log into Poe on any desktop web browser, then open your browser's developer tools (also known as "inspect") and look for the value of the p-b cookie in the following menus:

  • Chromium: Devtools > Application > Cookies > poe.com
  • Firefox: Devtools > Storage > Cookies
  • Safari: Devtools > Storage > Cookies

Using the Client:

To use this library, simply import poe and create a poe.Client instance. The Client class accepts the following arguments:

  • token - The token to use.
  • proxy = None - The proxy to use, in the format protocol://host:port. The socks5h protocol is recommended, as it also proxies the DNS queries.
  • device_id = None - The device ID to use. If this is not specified, it will be randomly generated and stored on the disk.
  • headers = headers - The headers to use. This defaults to the headers specified in poe.headers.
  • client_identifier = client_identifier - The client identifier that will be passed into the TLS client library. This defaults to the one specified in poe.client_identifier.

Regular Example:

import poe
client = poe.Client("TOKEN_HERE")

Proxied Example:

import poe
client = poe.Client("TOKEN_HERE", proxy="socks5h://178.62.100.151:59166")

Note that the following examples assume client is the name of your poe.Client instance. If the token is invalid, a RuntimeError will be raised.

Downloading the Available Bots:

The client downloads all of the available bots upon initialization and stores them within client.bots. A dictionary that maps bot codenames to their display names can be found at client.bot_names. If you want to refresh these values, you can call client.get_bots. This function takes the following arguments:

  • download_next_data = True - Whether or not to re-download the __NEXT_DATA__, which is required if the bot list has changed.
print(json.dumps(client.bot_names, indent=2))
"""
{
  "capybara": "Sage",
  "a2": "Claude-instant",
  "nutria": "Dragonfly",
  "a2_100k": "Claude-instant-100k",
  "beaver": "GPT-4",
  "chinchilla": "ChatGPT",
  "a2_2": "Claude+"
}
"""

Note that, on free accounts, Claude+ (a2_2) has a limit of 3 messages per day and GPT-4 (beaver) has a limit of 1 message per day. Claude-instant-100k (c2_100k) is completely inaccessible for free accounts. For all the other chatbots, there seems to be a rate limit of 10 messages per minute.

Using 3rd Party Bots:

To get a list of 3rd party bots, use client.explore_bots, which accepts the following arguments:

  • end_cursor = None - The cursor to use when fetching the list.
  • count = 25 - The number of bots that is returned.

The function will return a dict containing a list of bots and the cursor for the next page:

print(json.dumps(client.explore_bots(count=1), indent=2))
"""
{
  "bots": [
    {
      "id": "Qm90OjEwMzI2MDI=",
      "displayName": "leocooks",
      "deletionState": "not_deleted",
      "image": {
        "__typename": "UrlBotImage",
        "url": "https://qph.cf2.quoracdn.net/main-thumb-pb-1032602-200-uorvomwowfgmatdvrtwajtwwqlujmmgu.jpeg"
      },
      "botId": 1032602,
      "followerCount": 1922,
      "description": "Above average meals for average cooks, made simple by world-renowned chef, Leonardo",
      "__typename": "Bot"
    }
  ],
  "end_cursor": "1000172"
}
"""

To get a specific third party bot, you can use client.get_bot_by_codename, which accept's the bot's codename as its only argument.

client.get_bot_by_codename("JapaneseTutor")

Since display names are the same as the codenames for custom bots, you can simply pass the bot's display name into client.send_message to send it a message.

Creating New Bots:

You can create a new bot using the client.create_bot function, which accepts the following arguments:

  • handle - The handle of the new bot.
  • prompt = "" - The prompt for the new bot.
  • base_model = "chinchilla" - The model that the new bot uses. This must be either "chinchilla" (ChatGPT) or "a2" (Claude).
  • description = "" - The description for the new bot.
  • intro_message = "" - The intro message for the new bot. If this is an empty string then the bot will not have an intro message.
  • prompt_public = True - Whether or not the prompt should be publicly visible.
  • pfp_url = None - The URL for the bot's profile picture. Currently, there is no way to actually upload a custom image using this library.
  • linkification = False - Whether or not the bot should turn some text in the response into clickable links.
  • markdown_rendering = True - Whether or not to enable markdown rendering for the bot's responses.
  • suggested_replies = False - Whether or not the bot should suggest possible replies after each response.
  • private = False - Whether or not the bot should be private.

Use these arguments if you want the new bot to use your own API (as detailed here):

  • api_key = None - The API key for the new bot.
  • api_bot = False - Whether or not the bot has API functionally enabled.
  • api_url = None - The API URL for the new bot.

A full example of how to create and edit bots is located at examples/create_bot.py.

new_bot = client.create_bot(bot_name, "prompt goes here", base_model="a2")

Editing a Bot:

You can edit a custom bot using the client.edit_bot function, which accepts the following arguments:

  • bot_id - The botId of the bot to edit.
  • handle - The handle of the bot to edit.
  • prompt - The prompt for the new bot.
  • base_model = "chinchilla" - The new model that the bot uses. This must be either "chinchilla" (ChatGPT) or "a2" (Claude). Previously, it was possible to set this to "beaver" (GPT-4), which would bypass the free account restrictions, but this is now patched.
  • description = "" - The new description for the bot.
  • intro_message = "" - The new intro message for the bot. If this is an empty string then the bot will not have an intro message.
  • prompt_public = True - Whether or not the prompt should be publicly visible.
  • pfp_url = None - The URL for the bot's profile picture. Currently, there is no way to actually upload a custom image using this library.
  • linkification = False - Whether or not the bot should turn some text in the response into clickable links.
  • markdown_rendering = True - Whether or not to enable markdown rendering for the bot's responses.
  • suggested_replies = False - Whether or not the bot should suggest possible replies after each response.
  • private = False - Whether or not the bot should be private.

Bot API related arguments:

  • api_key = None - The new API key for the bot.
  • api_url = None - The new API URL for the bot.

A full example of how to create and edit bots is located at examples/create_bot.py.

edit_result = client.edit_bot(1086981, "bot_handle_here", base_model="a2")

Sending Messages:

You can use the client.send_message function to send a message to a chatbot, which accepts the following arguments:

  • chatbot - The codename of the chatbot. (example: capybara)
  • message - The message to send to the chatbot.
  • with_chat_break = False - Whether the conversation context should be cleared.
  • timeout = 20 - The max number of seconds in between received chunks until a RuntimeError is raised.

The function is a generator which returns the most recent version of the generated message whenever it is updated.

Streamed Example:

message = "Summarize the GNU GPL v3"
for chunk in client.send_message("capybara", message):
  print(chunk["text_new"], end="", flush=True)

Non-Streamed Example:

message = "Summarize the GNU GPL v3"
for chunk in client.send_message("capybara", message):
  pass
print(chunk["text"])

You can also send multiple messages in parallel using threading and receive their responses separately, as demonstrated in /examples/parallel_messages.py. Note that if you send messages too fast, the server will give an error, but the request will eventually succeed.

Clearing the Conversation Context:

If you want to clear the the context of a conversation without sending a message, you can use client.send_chat_break. The only argument is the codename of the bot whose context will be cleared.

client.send_chat_break("capybara")

The function returns the message which represents the chat break.

Downloading Conversation History:

To download past messages in a conversation, use the client.get_message_history function, which accepts the following arguments:

  • chatbot - The codename of the chatbot.
  • count = 25 - The number of messages to download.
  • cursor = None - The message ID to start at instead of the latest one.

Note that if you don't specify a cursor, the client will have to perform an extra request to determine what the latest cursor is.

The returned messages are ordered from oldest to newest.

message_history = client.get_message_history("capybara", count=10)
print(json.dumps(message_history, indent=2))
"""
[
  {
    "node": {
      "id": "TWVzc2FnZToxMDEwNzYyODU=",
      "messageId": 101076285,
      "creationTime": 1679298157718888,
      "text": "",
      "author": "chat_break",
      "linkifiedText": "",
      "state": "complete",
      "suggestedReplies": [],
      "vote": null,
      "voteReason": null,
      "__typename": "Message"
    },
    "cursor": "101076285",
    "id": "TWVzc2FnZUVkZ2U6MTAxMDc2Mjg1OjEwMTA3NjI4NQ=="
  },
  ...
]
"""

Deleting Messages:

To delete messages, use the client.delete_message function, which accepts a single argument. You can pass a single message ID into it to delete a single message, or you can pass a list of message IDs to delete multiple messages at once.

#delete a single message
client.delete_message(96105719)

#delete multiple messages at once
client.delete_message([96105719, 96097108, 96097078, 96084421, 96084402])

Purging a Conversation:

To purge an entire conversation, or just the last few messages, you can use the client.purge_conversation function. This function accepts the following arguments:

  • chatbot - The codename of the chatbot.
  • count = -1 - The number of messages to be deleted, starting from the latest one. The default behavior is to delete every single message.
#purge just the last 10 messages
client.purge_conversation("capybara", count=10)

#purge the entire conversation
client.purge_conversation("capybara")

Purging All Conversations:

To purge every conversation in your account, use the client.purge_all_conversations function. This function doesn't need any arguments.

>>> client.purge_all_conversations()

Getting the Remaining Messages:

To get the number of messages remaining in the quota for a conversation, use the client.get_remaining_messages function. This function accepts the following arguments:

  • chatbot - The codename of the chatbot.

The function will return the number of messages remaining, or None if the bot does not have a quota.

>>> client.get_remaining_messages("beaver")
1

Misc:

Changing the Logging Level:

If you want to show debug messages, simply call poe.logger.setLevel.

import poe
import logging
poe.logger.setLevel(logging.INFO)

Setting a Custom User-Agent:

If you want to change the headers that are spoofed, set poe.headers after importing the library.

To use your browser's own headers, visit this site, and copy-paste its contents.

import poe
poe.headers = {
  "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0",
  "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
  "Accept-Encoding": "gzip, deflate, br",
  "Accept-Language": "en-US,en;q=0.5",
  "Te": "trailers",
  "Upgrade-Insecure-Requests": "1"
}

The following headers will be ignored and overwritten:

{
  "Referrer": "https://poe.com/",
  "Origin": "https://poe.com",
  "Host": "poe.com",
  "Sec-Fetch-Dest": "empty",
  "Sec-Fetch-Mode": "cors",
  "Sec-Fetch-Site": "same-origin"
}

Previously, this was done through poe.user_agent, but that variable is now completely ignored.

You'd also want to change poe.client_identifier to match the user-agent that you have set. See the Python-TLS-Client documentation for some sample values. Keep in mind that spoofing Chrome/Firefox versions >= 110 may be detectable.

poe.client_identifier = "chrome_107"

Setting a Custom Device ID:

If you want to change the device ID that is being spoofed, you can use the poe.set_device_id, which accepts the following arguments:

  • user_id - The user ID of the account you want to change the device ID for. The user ID can be found at client.viewer["poeUser"]["id"].
  • device_id - The new device ID. This is a 32 character UUID string in the following format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
poe.set_device_id("UGMlVXqlcLYyMOATMDsKNTMz", "6d659b04-043a-41f8-97c7-fb7d7fe9ad34")

The device IDs are saved to ~/.config/poe-api/device_id.json on Unix-like systems, and C:\Users\<user>\AppData\Roaming\poe-api\device_id.json on Windows.

Additionally, the poe.get_device_id function or client.device_id can be used to retrieve the saved device ID.

>>> poe.get_device_id("UGMlVXqlcLYyMOATMDsKNTMz")
#6d659b04-043a-41f8-97c7-fb7d7fe9ad34

>>> client.device_id
#6d659b04-043a-41f8-97c7-fb7d7fe9ad34

Copyright:

This program is licensed under the GNU GPL v3. Most code, with the exception of the GraphQL queries, has been written by me, ading2210.

Reverse engineering the poe-tag-id header has been done by xtekky in PR #39.

The client.get_remaining_messages function was written by Snowad14 in PR #46.

Detection avoidance and fetching the third party bots has been done by acheong08 in PR #79.

Most of the GraphQL queries are taken from muharamdani/poe, which is licensed under the ISC License.

Copyright Notice:

ading2210/poe-api: a reverse engineered Python API wrapper for Quora's Poe
Copyright (C) 2023 ading2210

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.