diff --git a/README.md b/README.md index f6c0968..81e871a 100644 --- a/README.md +++ b/README.md @@ -122,10 +122,6 @@ Or you can install a proxy-support version of this library for **Python 3.9+** ```ShellSession pip install -U poe-api-wrapper[proxy] ``` -You can also use the Async version: -```ShellSession -pip install -U poe-api-wrapper[async] -``` Quick setup for Async Client: ```py from poe_api_wrapper import AsyncPoeApi @@ -208,8 +204,7 @@ Copy the values of `p-b` and `p-lat` cookies #### Getting formkey (*optional*) > [!IMPORTANT] -> **poe-api-wrapper** depends on library **js2py** to get formkey. If you are using `Python 3.12+`, it may not be compatible, so please downgrade your version. -> By default, poe_api_wrapper will automatically retrieve formkey for you. If it doesn't work, please pass this token manually by following these steps: +> By default, **poe-api-wrapper** will automatically retrieve formkey for you. If it doesn't work, please pass this token manually by following these steps: There are two ways to get formkey: @@ -521,30 +516,30 @@ print(client.get_botInfo(handle=bot)) ```py print(client.get_available_creation_models()) >> Output: -['chinchilla', 'mixtral8x7bchat', 'playgroundv25', 'stablediffusionxl', 'dalle3', 'a2', 'claude_2_short', 'gemini_pro', 'a2_2', 'a2_100k', 'beaver', 'llama_2_70b_chat', 'mythomaxl213b', 'claude_2_1_bamboo', 'claude_2_1_cedar', 'claude_3_haiku', 'claude_3_haiku_200k'] +['chinchilla', 'claude_3_haiku', 'mythomaxl213b', 'mixtral8x7bchat', 'playgroundv25', 'stablediffusionxl', 'dalle3', 'a2', 'claude_2_short', 'gemini_pro', 'a2_2', 'a2_100k', 'gpt4_o', 'gpt4_o_128k', 'beaver', 'vizcacha', 'gemini_1_5_pro', 'gemini_1_5_pro_128k', 'gemini_1_5_pro_1m', 'gemini_1_5_flash', 'gemini_1_5_flash_128k', 'gemini_1_5_flash_1m', 'llama_2_70b_chat', 'claude_2_1_bamboo', 'claude_2_1_cedar', 'claude_3_haiku_200k', 'claude_3_sonnet_200k', 'claude_3_opus_200k', 'sd3turbo', 'stablediffusion3', 'ideogram'] ``` - Creating a new Bot ```py -client.create_bot("BOT_NAME", "PROMPT_HERE", base_model="a2") +client.create_bot(handle="BOT_NAME", prompt="PROMPT_HERE", base_model="a2") # Using knowledge bases (you can use source_ids from uploaded knowledge bases for your custom bot) -client.create_bot("BOT_NAME", "PROMPT_HERE", base_model="a2", knowledgeSourceIds=source_ids, shouldCiteSources=True) +client.create_bot(handle="BOT_NAME", prompt="PROMPT_HERE", base_model="a2", knowledgeSourceIds=source_ids, shouldCiteSources=True) ``` - Editing a Bot ```py -client.edit_bot("(NEW)BOT_NAME", "PROMPT_HERE", base_model='chinchilla') +client.edit_bot(handle="BOT_NAME", prompt="PROMPT_HERE", new_handle="NEW_BOT_NAME", base_model='chinchilla') # Adding knowledge bases -client.edit_bot("(NEW)BOT_NAME", "PROMPT_HERE", base_model='chinchilla', knowledgeSourceIdsToAdd=source_ids, shouldCiteSources=True) +client.edit_bot(handle="BOT_NAME", prompt="PROMPT_HERE", new_handle="NEW_BOT_NAME", base_model='chinchilla', knowledgeSourceIdsToAdd=source_ids, shouldCiteSources=True) # Removing knowledge bases -client.edit_bot("(NEW)BOT_NAME", "PROMPT_HERE", base_model='chinchilla', knowledgeSourceIdsToRemove=source_ids, shouldCiteSources=True) +client.edit_bot(handle="BOT_NAME", prompt="PROMPT_HERE", new_handle="NEW_BOT_NAME", base_model='chinchilla', knowledgeSourceIdsToRemove=source_ids, shouldCiteSources=True) ``` > [!TIP] > You can also use both `knowledgeSourceIdsToAdd` and `knowledgeSourceIdsToRemove` at the same time. - Deleting a Bot ```py -client.delete_bot("BOT_NAME") +client.delete_bot(handle="BOT_NAME") ``` - Getting available bots (your bots section) ```py diff --git a/poe_api_wrapper/api.py b/poe_api_wrapper/api.py index 68c2ab6..ee82d30 100644 --- a/poe_api_wrapper/api.py +++ b/poe_api_wrapper/api.py @@ -1,7 +1,7 @@ from time import sleep, time from httpx import Client, ReadTimeout, ConnectError from requests_toolbelt import MultipartEncoder -import os, secrets, string, random, websocket, json, threading, queue, ssl, hashlib +import os, secrets, string, random, websocket, json, threading, queue, ssl, hashlib, re from loguru import logger from typing import Generator from .utils import ( @@ -1228,9 +1228,13 @@ def get_available_creation_models(self): return models def create_bot(self, handle, prompt, display_name=None, base_model="chinchilla", description="", intro_message="", - api_key=None, api_bot=False, api_url=None, prompt_public=True, pfp_url=None, linkification=False, - markdown_rendering=True, suggested_replies=False, private=False, temperature=None, customMessageLimit=None, - messagePriceCc=None, shouldCiteSources=True, knowledgeSourceIds:dict = {}): + api_key=None, api_bot=False, api_url=None, prompt_public=True, pfp_url=None, markdown_rendering=True, + suggested_replies=False, private=False, temperature=None, customMessageLimit=None, messagePriceCc=None, + shouldCiteSources=True, knowledgeSourceIds:dict = {}, allowRelatedBotRecommendations=True): + + if not re.match("^[a-zA-Z0-9_.-]{4,20}$", handle): + raise ValueError("Invalid handle. Should be unique and use 4-20 characters, including letters, numbers, dashes, periods and underscores.") + bot_models = self.get_available_creation_models() if base_model not in bot_models: raise ValueError(f"Invalid base model {base_model}. Please choose from {bot_models}") @@ -1243,6 +1247,7 @@ def create_bot(self, handle, prompt, display_name=None, base_model="chinchilla", sourceIds = [item for sublist in knowledgeSourceIds.values() for item in sublist] else: sourceIds = [] + variables = { "model": base_model, "displayName": display_name, @@ -1255,7 +1260,6 @@ def create_bot(self, handle, prompt, display_name=None, base_model="chinchilla", "apiUrl": api_url, "apiKey": api_key, "isApiBot": api_bot, - "hasLinkification": linkification, "hasMarkdownRendering": markdown_rendering, "hasSuggestedReplies": suggested_replies, "isPrivateBot": private, @@ -1263,8 +1267,10 @@ def create_bot(self, handle, prompt, display_name=None, base_model="chinchilla", "customMessageLimit": customMessageLimit, "knowledgeSourceIds": sourceIds, "messagePriceCc": messagePriceCc, - "shouldCiteSources": shouldCiteSources + "shouldCiteSources": shouldCiteSources, + "allowRelatedBotRecommendations": allowRelatedBotRecommendations, } + result = self.send_request('gql_POST', 'PoeBotCreate', variables)['data']['poeBotCreate'] if result["status"] != "success": logger.error(f"Poe returned an error while trying to create a bot: {result['status']}") @@ -1282,11 +1288,15 @@ def get_botData(self, handle): f"Fail to get botId from {handle}. Make sure the bot exists and you have access to it." ) from e - def edit_bot(self, handle, prompt, display_name=None, base_model="chinchilla", description="", - intro_message="", api_key=None, api_url=None, private=False, - prompt_public=True, pfp_url=None, linkification=False, - markdown_rendering=True, suggested_replies=False, temperature=None, customMessageLimit=None, - knowledgeSourceIdsToAdd:dict = {}, knowledgeSourceIdsToRemove:dict = {}, messagePriceCc=None, shouldCiteSources=True): + def edit_bot(self, handle, prompt, new_handle=None, display_name=None, base_model="chinchilla", description="", + intro_message="", api_key=None, api_url=None, private=False, prompt_public=True, + pfp_url=None, markdown_rendering=True, suggested_replies=False, temperature=None, + customMessageLimit=None, knowledgeSourceIdsToAdd:dict = {}, knowledgeSourceIdsToRemove:dict = {}, + messagePriceCc=None, shouldCiteSources=True, allowRelatedBotRecommendations=True): + + if new_handle and not re.match("^[a-zA-Z0-9_.-]{4,20}$", new_handle): + raise ValueError("Invalid handle. Should be unique and use 4-20 characters, including letters, numbers, dashes, periods and underscores.") + bot_models = self.get_available_creation_models() if base_model not in bot_models: raise ValueError(f"Invalid base model {base_model}. Please choose from {bot_models}") @@ -1299,34 +1309,50 @@ def edit_bot(self, handle, prompt, display_name=None, base_model="chinchilla", d removeIds = [item for sublist in knowledgeSourceIdsToRemove.values() for item in sublist] else: removeIds = [] + + try: + botId = self.get_botData(handle)['botId'] + except Exception as e: + raise ValueError( + f"Fail to get botId from {handle}. Make sure the bot exists and you have access to it." + ) from e + variables = { - "baseBot": base_model, - "botId": self.get_botData(handle)['botId'], - "handle": handle, - "displayName": display_name, - "prompt": prompt, - "isPromptPublic": prompt_public, - "introduction": intro_message, - "description": description, - "profilePictureUrl": pfp_url, - "apiUrl": api_url, - "apiKey": api_key, - "hasLinkification": linkification, - "hasMarkdownRendering": markdown_rendering, - "hasSuggestedReplies": suggested_replies, - "isPrivateBot": private, - "temperature": temperature, - "customMessageLimit": customMessageLimit, - "knowledgeSourceIdsToAdd": addIds, - "knowledgeSourceIdsToRemove": removeIds, - "messagePriceCc": messagePriceCc, - "shouldCiteSources": shouldCiteSources + "baseBot": base_model, + "botId": botId, + "handle": new_handle if new_handle != None else handle, + "displayName": display_name, + "prompt": prompt, + "isPromptPublic": prompt_public, + "introduction": intro_message, + "description": description, + "profilePictureUrl": pfp_url, + "apiUrl": api_url, + "apiKey": api_key, + "hasMarkdownRendering": markdown_rendering, + "hasSuggestedReplies": suggested_replies, + "isPrivateBot": private, + "temperature": temperature, + "customMessageLimit": customMessageLimit, + "knowledgeSourceIdsToAdd": addIds, + "knowledgeSourceIdsToRemove": removeIds, + "messagePriceCc": messagePriceCc, + "shouldCiteSources": shouldCiteSources, + "allowRelatedBotRecommendations": allowRelatedBotRecommendations, } + result = self.send_request('gql_POST', 'PoeBotEdit', variables)["data"]["poeBotEdit"] if result["status"] != "success": logger.error(f"Poe returned an error while trying to edit a bot: {result['status']}") else: - logger.info(f"Bot edited successfully | {handle}") + if new_handle and handle != new_handle: + if handle in self.current_thread: + self.current_thread[new_handle] = self.current_thread.pop(handle) + logger.info(f"Bot edited successfully | New handle from {handle} to {new_handle}") + else: + logger.info(f"Bot edited successfully | {handle}") + + return {"status": result["status"], "botId": botId, "handle": new_handle if new_handle != None else handle} def delete_bot(self, handle): isCreator = self.get_botData(handle)['viewerIsCreator'] diff --git a/poe_api_wrapper/async_api.py b/poe_api_wrapper/async_api.py index c4348e1..c343d97 100644 --- a/poe_api_wrapper/async_api.py +++ b/poe_api_wrapper/async_api.py @@ -3,7 +3,7 @@ ASYNC = True except ImportError: ASYNC = False -import asyncio, json, queue, random, ssl, threading, websocket, string, secrets, os, hashlib +import asyncio, json, queue, random, ssl, threading, websocket, string, secrets, os, hashlib, re from time import time from typing import AsyncIterator from loguru import logger @@ -1227,9 +1227,13 @@ async def get_available_creation_models(self): return models async def create_bot(self, handle, prompt, display_name=None, base_model="chinchilla", description="", intro_message="", - api_key=None, api_bot=False, api_url=None, prompt_public=True, pfp_url=None, linkification=False, - markdown_rendering=True, suggested_replies=False, private=False, temperature=None, customMessageLimit=None, - messagePriceCc=None, shouldCiteSources=True, knowledgeSourceIds:dict = {}): + api_key=None, api_bot=False, api_url=None, prompt_public=True, pfp_url=None, markdown_rendering=True, + suggested_replies=False, private=False, temperature=None, customMessageLimit=None, messagePriceCc=None, + shouldCiteSources=True, knowledgeSourceIds:dict = {}, allowRelatedBotRecommendations=True): + + if not re.match("^[a-zA-Z0-9_.-]{4,20}$", handle): + raise ValueError("Invalid handle. Should be unique and use 4-20 characters, including letters, numbers, dashes, periods and underscores.") + bot_models = await self.get_available_creation_models() if base_model not in bot_models: raise ValueError(f"Invalid base model {base_model}. Please choose from {bot_models}") @@ -1242,6 +1246,7 @@ async def create_bot(self, handle, prompt, display_name=None, base_model="chinch sourceIds = [item for sublist in knowledgeSourceIds.values() for item in sublist] else: sourceIds = [] + variables = { "model": base_model, "displayName": display_name, @@ -1254,7 +1259,6 @@ async def create_bot(self, handle, prompt, display_name=None, base_model="chinch "apiUrl": api_url, "apiKey": api_key, "isApiBot": api_bot, - "hasLinkification": linkification, "hasMarkdownRendering": markdown_rendering, "hasSuggestedReplies": suggested_replies, "isPrivateBot": private, @@ -1262,8 +1266,10 @@ async def create_bot(self, handle, prompt, display_name=None, base_model="chinch "customMessageLimit": customMessageLimit, "knowledgeSourceIds": sourceIds, "messagePriceCc": messagePriceCc, - "shouldCiteSources": shouldCiteSources + "shouldCiteSources": shouldCiteSources, + "allowRelatedBotRecommendations": allowRelatedBotRecommendations, } + result = await self.send_request('gql_POST', 'PoeBotCreate', variables)['data']['poeBotCreate'] if result["status"] != "success": logger.error(f"Poe returned an error while trying to create a bot: {result['status']}") @@ -1281,12 +1287,16 @@ async def get_botData(self, handle): f"Fail to get botId from {handle}. Make sure the bot exists and you have access to it." ) from e - async def edit_bot(self, handle, prompt, display_name=None, base_model="chinchilla", description="", - intro_message="", api_key=None, api_url=None, private=False, - prompt_public=True, pfp_url=None, linkification=False, - markdown_rendering=True, suggested_replies=False, temperature=None, customMessageLimit=None, - knowledgeSourceIdsToAdd:dict = {}, knowledgeSourceIdsToRemove:dict = {}, messagePriceCc=None, shouldCiteSources=True): - bot_models = await self.get_available_creation_models() + async def edit_bot(self, handle, prompt, new_handle=None, display_name=None, base_model="chinchilla", description="", + intro_message="", api_key=None, api_url=None, private=False, prompt_public=True, + pfp_url=None, markdown_rendering=True, suggested_replies=False, temperature=None, + customMessageLimit=None, knowledgeSourceIdsToAdd:dict = {}, knowledgeSourceIdsToRemove:dict = {}, + messagePriceCc=None, shouldCiteSources=True, allowRelatedBotRecommendations=True): + + if new_handle and not re.match("^[a-zA-Z0-9_.-]{4,20}$", new_handle): + raise ValueError("Invalid handle. Should be unique and use 4-20 characters, including letters, numbers, dashes, periods and underscores.") + + bot_models = await self.get_available_creation_models() if base_model not in bot_models: raise ValueError(f"Invalid base model {base_model}. Please choose from {bot_models}") @@ -1298,35 +1308,51 @@ async def edit_bot(self, handle, prompt, display_name=None, base_model="chinchil removeIds = [item for sublist in knowledgeSourceIdsToRemove.values() for item in sublist] else: removeIds = [] + + try: + botId = await self.get_botData(handle)['botId'] + except Exception as e: + raise ValueError( + f"Fail to get botId from {handle}. Make sure the bot exists and you have access to it." + ) from e + variables = { - "baseBot": base_model, - "botId": await self.get_botData(handle)['botId'], - "handle": handle, - "displayName": display_name, - "prompt": prompt, - "isPromptPublic": prompt_public, - "introduction": intro_message, - "description": description, - "profilePictureUrl": pfp_url, - "apiUrl": api_url, - "apiKey": api_key, - "hasLinkification": linkification, - "hasMarkdownRendering": markdown_rendering, - "hasSuggestedReplies": suggested_replies, - "isPrivateBot": private, - "temperature": temperature, - "customMessageLimit": customMessageLimit, - "knowledgeSourceIdsToAdd": addIds, - "knowledgeSourceIdsToRemove": removeIds, - "messagePriceCc": messagePriceCc, - "shouldCiteSources": shouldCiteSources + "baseBot": base_model, + "botId": botId, + "handle": new_handle if new_handle != None else handle, + "displayName": display_name, + "prompt": prompt, + "isPromptPublic": prompt_public, + "introduction": intro_message, + "description": description, + "profilePictureUrl": pfp_url, + "apiUrl": api_url, + "apiKey": api_key, + "hasMarkdownRendering": markdown_rendering, + "hasSuggestedReplies": suggested_replies, + "isPrivateBot": private, + "temperature": temperature, + "customMessageLimit": customMessageLimit, + "knowledgeSourceIdsToAdd": addIds, + "knowledgeSourceIdsToRemove": removeIds, + "messagePriceCc": messagePriceCc, + "shouldCiteSources": shouldCiteSources, + "allowRelatedBotRecommendations": allowRelatedBotRecommendations, } + result = await self.send_request('gql_POST', 'PoeBotEdit', variables)["data"]["poeBotEdit"] if result["status"] != "success": logger.error(f"Poe returned an error while trying to edit a bot: {result['status']}") else: - logger.info(f"Bot edited successfully | {handle}") - + if new_handle and handle != new_handle: + if handle in self.current_thread: + self.current_thread[new_handle] = self.current_thread.pop(handle) + logger.info(f"Bot edited successfully | New handle from {handle} to {new_handle}") + else: + logger.info(f"Bot edited successfully | {handle}") + + return {"status": result["status"], "botId": botId, "handle": new_handle if new_handle != None else handle} + async def delete_bot(self, handle): isCreator = await self.get_botData(handle)['viewerIsCreator'] botId = await self.get_botData(handle)['botId'] diff --git a/poe_api_wrapper/bundles.py b/poe_api_wrapper/bundles.py index 3a28671..1792f2b 100644 --- a/poe_api_wrapper/bundles.py +++ b/poe_api_wrapper/bundles.py @@ -1,7 +1,7 @@ from httpx import Client from bs4 import BeautifulSoup -from js2py import eval_js from loguru import logger +import quickjs import re class PoeBundle: @@ -77,6 +77,7 @@ def get_form_key(self) -> str: raise RuntimeError("Failed to parse form-key function in Poe document") script += f'window.{secret}().slice(0, 32);' - formkey = str(eval_js(script)) + context = quickjs.Context() + formkey = str(context.eval(script)) logger.info(f"Retrieved formkey successfully: {formkey}") return formkey diff --git a/poe_api_wrapper/queries.py b/poe_api_wrapper/queries.py index 050373b..3dc94b8 100644 --- a/poe_api_wrapper/queries.py +++ b/poe_api_wrapper/queries.py @@ -34,7 +34,7 @@ "ContinueChatFromPoeShare": "a220810c2d1d3b5284b6be44309a3d2b197c312a79b1a27f165a12f1508322bd", "ContinueChatIndexPageQuery": "a7eea6eebd14aa355723514558762315d0b4df46205b70f825d288d5ed1635ec", "ContinueChatPageQuery": "fe3a4d2006b1c4bb47ac6dea0639bc9128ad983cf37cbc0006c33efab372a19d", - "CreateBotIndexPageQuery": "0d7db3055a91d8574628b81f4e5ed23309bacb90cab08ac3ab7e32d67a72fd22", + "CreateBotIndexPageQuery": "9dbcad9827cb9312c8f1a6f3ac71fc455348147738d943de4499bb73f31ff48c", "CreateBotPageQuery": "4fa5e0703c416fc6b40c5e2fcfcac66301ed0c8d32bafb5d69300e7553ef1f8f", "CreateChatMutation": "f1322c9c34d4140d420aeb9151cdeebc2235d381ada0037c845572310f613b7d", "CreateChatWithTitle": "6cf8cddd6594901bd4a7dc6ddeffc91485c21c817e8f7fa07fae9d71d9807d71", @@ -99,9 +99,10 @@ "PagesDefaultBotChatPageQuery": "75bd0877369c2b4191c936572822ce1875c980f1f6f8683381bd4a6850bdea92", "BotInfoCardActionBar_poeBotDelete_Mutation": "ddda605feb83223640499942fac70c440d6767d48d8ff1a26543f37c9bb89c68", "BotInfoCardActionBar_poeRemoveBotFromUserList_Mutation": "5dd4da93f99a2daf629f082b6a4938d940833e8054b5f4f611d9c8c1928b0dc4", - "PoeBotCreate": "384184cd2e904bb0da2ce55ad2b36fd320463e52572147f6663aa53be26cc8e7", - "PoeBotDelete": "b446d0e94980e36d9ba7a5bc3188850186069d529b4c337fb9e91b9ead876c12", - "PoeBotEdit": "715949794448a572858f879b09730008f41cbea1c1934f4b53268d8ffc97c6e0", + "BotInfoForm_NumTokensFromPromptQuery": "1d9bef79811f3b2ddca5ce4027b7eaa31a51bbeed1edf8b6f72e2e0d09d80609", + "PoeBotCreate": "bfd53c9e38dd07affcdaabe2108f7f457a2ce8b27d52c02e4e48dd66cf016913", + "PoeBotDelete": "964dd6a76806babf0056151c85dd3fde36f03e9007bd0babf2a90a7bb3a67891", + "PoeBotEdit": "6a7a7959ecde12e3782266c584de5d514ebbdecfe9435bbccb3fb7db1962e6c6", "PoeBotFollow": "efa3f25f6cd67f9dea757be50305c0caa6a4e51f52ffba7e4a1c1f2c84d6dbd0", "PoeBotUnfollow": "db2281f3efa305db62d38964b640e982076491c2c59d5be3303feae343fe8914", "PoeRemoveBotFromUserList": "89e756b668b2318fa73c2a9dde4608a4529c74844667417c0cfb245e7e04e96e", diff --git a/setup.py b/setup.py index de1b9e8..c8fe1d0 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ base_path = Path(__file__).parent long_description = (base_path / "README.md").read_text(encoding='utf-8') -VERSION = '1.4.6' +VERSION = '1.4.7' DESCRIPTION = 'A simple, lightweight and efficient API wrapper for Poe.com' LONG_DESCRIPTION = '👾 A Python API wrapper for Poe.com. With this, you will have free access to ChatGPT, Claude, Llama, Gemini, Google-PaLM and more! 🚀' @@ -17,9 +17,8 @@ long_description=long_description, packages=find_packages(), python_requires=">=3.7", - install_requires=['httpx[http2]', 'websocket-client', 'requests_toolbelt', 'loguru', 'rich==13.3.4', 'bs4', 'Js2Py'], + install_requires=['httpx[http2]', 'websocket-client', 'requests_toolbelt', 'loguru', 'rich==13.3.4', 'beautifulsoup4', 'quickjs', 'nest-asyncio'], extras_require={ - 'async': ['nest-asyncio'], 'proxy': ['ballyregan; python_version>="3.9"'], 'tests': ['tox'], },