├── LICENSE ├── README.md ├── images ├── emailnator.jpg ├── emailnator.png └── perplexity.png ├── perplexity ├── __init__.py ├── client.py ├── driver.py ├── emailnator.py └── labs.py ├── perplexity_async ├── __init__.py ├── client.py ├── emailnator.py └── labs.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 helallao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Perplexity AI 2 | 3 | Perplexity AI is a Python module that leverages [Emailnator](https://emailnator.com/) to generate new accounts for unlimited pro queries. It supports both synchronous and asynchronous APIs, as well as a web interface for users who prefer a GUI-based approach. 4 | 5 | ## Features 6 | 7 | - **Account Generation**: Automatically generate Gmail accounts using Emailnator. 8 | - **Unlimited Pro Queries**: Bypass query limits by creating new accounts. 9 | - **Web Interface**: Automate account creation and usage via a browser. 10 | - **API Support**: Synchronous and asynchronous APIs for programmatic access. 11 | 12 | ## Installation 13 | 14 | Install the required packages: 15 | 16 | ```bash 17 | pip install perplexity-api perplexity-api-async 18 | ``` 19 | 20 | For the web interface, install additional dependencies: 21 | 22 | ```bash 23 | pip install patchright playwright && patchright install chromium 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### Web Interface 29 | 30 | The web interface automates account creation and usage in a browser. [Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python#best-practices) uses ["Chrome User Data Directory"](https://www.google.com/search?q=chrome+user+data+directory) to be completely undetected, it's ``C:\Users\YourName\AppData\Local\Google\Chrome\User Data`` for Windows, as shown below: 31 | 32 | ```python 33 | import os 34 | from perplexity.driver import Driver 35 | 36 | cli = Driver() 37 | cli.run(rf'C:\\Users\\{os.getlogin()}\\AppData\\Local\\Google\\Chrome\\User Data') 38 | ``` 39 | 40 | To use your own Chrome instance, enable remote debugging (it may enter dead loop in Cloudflare): 41 | 42 | 1. Add `--remote-debugging-port=9222` to Chrome's shortcut target. 43 | 2. Pass the port to the `Driver.run()` method: 44 | 45 | ```python 46 | cli.run(rf'C:\\Users\\{os.getlogin()}\\AppData\\Local\\Google\\Chrome\\User Data', port=9222) 47 | ``` 48 | 49 | ### API Usage 50 | 51 | #### Synchronous API 52 | 53 | Below is an example code for simple usage, without using your own account or generating new accounts. 54 | 55 | ```python3 56 | import perplexity 57 | 58 | perplexity_cli = perplexity.Client() 59 | 60 | # mode = ['auto', 'pro', 'reasoning', 'deep research'] 61 | # model = model for mode, which can only be used in own accounts, that is { 62 | # 'auto': [None], 63 | # 'pro': [None, 'sonar', 'gpt-4.5', 'gpt-4o', 'claude 3.7 sonnet', 'gemini 2.0 flash', 'grok-2'], 64 | # 'reasoning': [None, 'r1', 'o3-mini', 'claude 3.7 sonnet'], 65 | # 'deep research': [None] 66 | # } 67 | # sources = ['web', 'scholar', 'social'] 68 | # files = a dictionary which has keys as filenames and values as file data 69 | # stream = returns a generator when enabled and just final response when disabled 70 | # language = ISO 639 code of language you want to use 71 | # follow_up = last query info for follow-up queries, you can directly pass response from a query, look at second example below 72 | # incognito = Enables incognito mode, for people who are using their own account 73 | resp = perplexity_cli.search('Your query here', mode='auto', model=None, sources=['web'], files={}, stream=False, language='en-US', follow_up=None, incognito=False) 74 | print(resp) 75 | 76 | # second example to show how to use follow-up queries and stream response 77 | for i in perplexity_cli.search('Your query here', stream=True, follow_up=resp): 78 | print(i) 79 | ``` 80 | 81 | And this is how you use your own account, you need to get your cookies in order to use your own account. Look at [How To Get Cookies](#how-to-get-cookies), 82 | 83 | ```python3 84 | import perplexity 85 | 86 | perplexity_cookies = { 87 | 88 | } 89 | 90 | perplexity_cli = perplexity.Client(perplexity_cookies) 91 | 92 | resp = perplexity_cli.search('Your query here', mode='reasoning', model='o3-mini', sources=['web'], files={'myfile.txt': open('file.txt').read()}, stream=False, language='en-US', follow_up=None, incognito=False) 93 | print(resp) 94 | ``` 95 | 96 | And finally account generating, you need to get cookies for [Emailnator](https://emailnator.com/) to use this feature. Look at [How To Get Cookies](#how-to-get-cookies), 97 | 98 | ```python3 99 | import perplexity 100 | 101 | emailnator_cookies = { 102 | 103 | } 104 | 105 | perplexity_cli = perplexity.Client() 106 | perplexity_cli.create_account(emailnator_cookies) # Creates a new gmail, so your 5 pro queries will be renewed. 107 | 108 | resp = perplexity_cli.search('Your query here', mode='reasoning', model=None, sources=['web'], files={'myfile.txt': open('file.txt').read()}, stream=False, language='en-US', follow_up=None, incognito=False) 109 | print(resp) 110 | ``` 111 | 112 | #### Asynchronous API 113 | 114 | Below is an example code for simple usage, without using your own account or generating new accounts. 115 | 116 | ```python3 117 | import asyncio 118 | import perplexity_async 119 | 120 | async def test(): 121 | perplexity_cli = await perplexity_async.Client() 122 | 123 | # mode = ['auto', 'pro', 'reasoning', 'deep research'] 124 | # model = model for mode, which can only be used in own accounts, that is { 125 | # 'auto': [None], 126 | # 'pro': [None, 'sonar', 'gpt-4.5', 'gpt-4o', 'claude 3.7 sonnet', 'gemini 2.0 flash', 'grok-2'], 127 | # 'reasoning': [None, 'r1', 'o3-mini', 'claude 3.7 sonnet'], 128 | # 'deep research': [None] 129 | # } 130 | # sources = ['web', 'scholar', 'social'] 131 | # files = a dictionary which has keys as filenames and values as file data 132 | # stream = returns a generator when enabled and just final response when disabled 133 | # language = ISO 639 code of language you want to use 134 | # follow_up = last query info for follow-up queries, you can directly pass response from a query, look at second example below 135 | # incognito = Enables incognito mode, for people who are using their own account 136 | resp = await perplexity_cli.search('Your query here', mode='auto', model=None, sources=['web'], files={}, stream=False, language='en-US', follow_up=None, incognito=False) 137 | print(resp) 138 | 139 | # second example to show how to use follow-up queries and stream response 140 | async for i in await perplexity_cli.search('Your query here', stream=True, follow_up=resp): 141 | print(i) 142 | 143 | asyncio.run(test()) 144 | ``` 145 | 146 | And this is how you use your own account, you need to get your cookies in order to use your own account. Look at [How To Get The Cookies](#how-to-get-the-cookies), 147 | 148 | ```python3 149 | import asyncio 150 | import perplexity_async 151 | 152 | perplexity_cookies = { 153 | 154 | } 155 | 156 | async def test(): 157 | perplexity_cli = await perplexity_async.Client(perplexity_cookies) 158 | 159 | resp = await perplexity_cli.search('Your query here', mode='reasoning', model='o3-mini', sources=['web'], files={'myfile.txt': open('file.txt').read()}, stream=False, language='en-US', follow_up=None, incognito=False) 160 | print(resp) 161 | 162 | asyncio.run(test()) 163 | ``` 164 | 165 | And finally account generating, you need to get cookies for [emailnator](https://emailnator.com/) to use this feature. Look at [How To Get The Cookies](#how-to-get-the-cookies), 166 | 167 | ```python3 168 | import asyncio 169 | import perplexity_async 170 | 171 | emailnator_cookies = { 172 | 173 | } 174 | 175 | async def test(): 176 | perplexity_cli = await perplexity_async.Client() 177 | await perplexity_cli.create_account(emailnator_cookies) # Creates a new gmail, so your 5 pro queries will be renewed. 178 | 179 | resp = await perplexity_cli.search('Your query here', mode='reasoning', model=None, sources=['web'], files={'myfile.txt': open('file.txt').read()}, stream=False, language='en-US', follow_up=None, incognito=False) 180 | print(resp) 181 | 182 | asyncio.run(test()) 183 | ``` 184 | 185 | ## How to Get Cookies 186 | 187 | ### Perplexity (to use your own account) 188 | * Open [Perplexity.ai](https://perplexity.ai/) website and login to your account. 189 | * Click F12 or ``Ctrl + Shift + I`` to open inspector. 190 | * Go to the "Network" tab in the inspector. 191 | * Refresh the page, right click the first request, hover on "Copy" and click to "Copy as cURL (bash)". 192 | * Now go to the [CurlConverter](https://curlconverter.com/python/) and paste your code here. The cookies dictionary will appear, copy and use it in your codes. 193 | 194 | 195 | 196 | ### Emailnator (for account generating) 197 | * Open [Emailnator](https://emailnator.com/) website and verify you're human. 198 | * Click F12 or ``Ctrl + Shift + I`` to open inspector. 199 | * Go to the "Network" tab in the inspector. 200 | * Refresh the page, right click the first request, hover on "Copy" and click to "Copy as cURL (bash)". 201 | * Now go to the [CurlConverter](https://curlconverter.com/python/) and paste your code here. The cookies dictionary will appear, copy and use it in your codes. 202 | * Cookies for [Emailnator](https://emailnator.com/) are temporary, you need to renew them continuously. 203 | 204 | 205 | 206 | ## License 207 | 208 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 209 | -------------------------------------------------------------------------------- /images/emailnator.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helallao/perplexity-ai/d7306ffed8dbb4f42afbb4d42bac5b2063353a8b/images/emailnator.jpg -------------------------------------------------------------------------------- /images/emailnator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helallao/perplexity-ai/d7306ffed8dbb4f42afbb4d42bac5b2063353a8b/images/emailnator.png -------------------------------------------------------------------------------- /images/perplexity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helallao/perplexity-ai/d7306ffed8dbb4f42afbb4d42bac5b2063353a8b/images/perplexity.png -------------------------------------------------------------------------------- /perplexity/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .emailnator import Emailnator 3 | from .labs import LabsClient 4 | 5 | __all__ = ['Client', 'Emailnator', 'LabsClient'] -------------------------------------------------------------------------------- /perplexity/client.py: -------------------------------------------------------------------------------- 1 | # Importing necessary modules 2 | # re: Regular expressions for pattern matching 3 | # sys: System-specific parameters and functions 4 | # json: JSON parsing and serialization 5 | # random: Random number generation 6 | # mimetypes: Guessing MIME types of files 7 | # uuid: Generating unique identifiers 8 | # curl_cffi: HTTP requests and multipart form data handling 9 | import re 10 | import sys 11 | import json 12 | import random 13 | import mimetypes 14 | from uuid import uuid4 15 | from curl_cffi import requests, CurlMime 16 | 17 | # Importing Emailnator class for email generation 18 | from .emailnator import Emailnator 19 | 20 | class Client: 21 | ''' 22 | A client for interacting with the Perplexity AI API. 23 | ''' 24 | 25 | def __init__(self, cookies={}): 26 | # Initialize an HTTP session with default headers and optional cookies 27 | self.session = requests.Session(headers={ 28 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 29 | 'accept-language': 'en-US,en;q=0.9', 30 | 'cache-control': 'max-age=0', 31 | 'dnt': '1', 32 | 'priority': 'u=0, i', 33 | 'sec-ch-ua': '"Not;A=Brand";v="24", "Chromium";v="128"', 34 | 'sec-ch-ua-arch': '"x86"', 35 | 'sec-ch-ua-bitness': '"64"', 36 | 'sec-ch-ua-full-version': '"128.0.6613.120"', 37 | 'sec-ch-ua-full-version-list': '"Not;A=Brand";v="24.0.0.0", "Chromium";v="128.0.6613.120"', 38 | 'sec-ch-ua-mobile': '?0', 39 | 'sec-ch-ua-model': '""', 40 | 'sec-ch-ua-platform': '"Windows"', 41 | 'sec-ch-ua-platform-version': '"19.0.0"', 42 | 'sec-fetch-dest': 'document', 43 | 'sec-fetch-mode': 'navigate', 44 | 'sec-fetch-site': 'same-origin', 45 | 'sec-fetch-user': '?1', 46 | 'upgrade-insecure-requests': '1', 47 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 48 | }, cookies=cookies, impersonate='chrome') 49 | 50 | # Flags and counters for account and query management 51 | self.own = bool(cookies) # Indicates if the client uses its own account 52 | self.copilot = 0 if not cookies else float('inf') # Remaining pro queries 53 | self.file_upload = 0 if not cookies else float('inf') # Remaining file uploads 54 | 55 | # Regular expression for extracting sign-in links 56 | self.signin_regex = re.compile(r'"(https://www\\.perplexity\\.ai/api/auth/callback/email\\?callbackUrl=.*?)"') 57 | 58 | # Unique timestamp for session identification 59 | self.timestamp = format(random.getrandbits(32), '08x') 60 | 61 | # Initialize session by making a GET request 62 | self.session.get('https://www.perplexity.ai/api/auth/session') 63 | 64 | def create_account(self, cookies): 65 | ''' 66 | Creates a new account using Emailnator cookies. 67 | ''' 68 | while True: 69 | try: 70 | # Initialize Emailnator client 71 | emailnator_cli = Emailnator(cookies) 72 | 73 | # Send a POST request to initiate account creation 74 | resp = self.session.post('https://www.perplexity.ai/api/auth/signin/email', data={ 75 | 'email': emailnator_cli.email, 76 | 'csrfToken': self.session.cookies.get_dict()['next-auth.csrf-token'].split('%')[0], 77 | 'callbackUrl': 'https://www.perplexity.ai/', 78 | 'json': 'true' 79 | }) 80 | 81 | # Check if the response is successful 82 | if resp.ok: 83 | # Wait for the sign-in email to arrive 84 | new_msgs = emailnator_cli.reload(wait_for=lambda x: x['subject'] == 'Sign in to Perplexity', timeout=20) 85 | 86 | if new_msgs: 87 | break 88 | else: 89 | print('Perplexity account creating error:', resp) 90 | 91 | except Exception: 92 | pass 93 | 94 | # Extract the sign-in link from the email 95 | msg = emailnator_cli.get(func=lambda x: x['subject'] == 'Sign in to Perplexity') 96 | new_account_link = self.signin_regex.search(emailnator_cli.open(msg['messageID'])).group(1) 97 | 98 | # Complete the account creation process 99 | self.session.get(new_account_link) 100 | 101 | # Update query and file upload limits 102 | self.copilot = 5 103 | self.file_upload = 10 104 | 105 | return True 106 | 107 | def search(self, query, mode='auto', model=None, sources=['web'], files={}, stream=False, language='en-US', follow_up=None, incognito=False): 108 | ''' 109 | Executes a search query on Perplexity AI. 110 | 111 | Parameters: 112 | - query: The search query string. 113 | - mode: Search mode ('auto', 'pro', 'reasoning', 'deep research'). 114 | - model: Specific model to use for the query. 115 | - sources: List of sources ('web', 'scholar', 'social'). 116 | - files: Dictionary of files to upload. 117 | - stream: Whether to stream the response. 118 | - language: Language code (ISO 639). 119 | - follow_up: Information for follow-up queries. 120 | - incognito: Whether to enable incognito mode. 121 | ''' 122 | # Validate input parameters 123 | assert mode in ['auto', 'pro', 'reasoning', 'deep research'], 'Invalid search mode.' 124 | assert model in { 125 | 'auto': [None], 126 | 'pro': [None, 'sonar', 'gpt-4.5', 'gpt-4o', 'claude 3.7 sonnet', 'gemini 2.0 flash', 'grok-2'], 127 | 'reasoning': [None, 'r1', 'o3-mini', 'claude 3.7 sonnet'], 128 | 'deep research': [None] 129 | }[mode] if self.own else True, 'Invalid model for the selected mode.' 130 | assert all([source in ('web', 'scholar', 'social') for source in sources]), 'Invalid sources.' 131 | assert self.copilot > 0 if mode in ['pro', 'reasoning', 'deep research'] else True, 'No remaining pro queries.' 132 | assert self.file_upload - len(files) >= 0 if files else True, 'File upload limit exceeded.' 133 | 134 | # Update query and file upload counters 135 | self.copilot = self.copilot - 1 if mode in ['pro', 'reasoning', 'deep research'] else self.copilot 136 | self.file_upload = self.file_upload - len(files) if files else self.file_upload 137 | 138 | # Upload files and prepare the query payload 139 | uploaded_files = [] 140 | for filename, file in files.items(): 141 | file_type = mimetypes.guess_type(filename)[0] 142 | file_upload_info = (self.session.post( 143 | 'https://www.perplexity.ai/rest/uploads/create_upload_url?version=2.18&source=default', 144 | json={ 145 | 'content_type': file_type, 146 | 'file_size': sys.getsizeof(file), 147 | 'filename': filename, 148 | 'force_image': False, 149 | 'source': 'default', 150 | } 151 | )).json() 152 | 153 | # Upload the file to the server 154 | mp = CurlMime() 155 | for key, value in file_upload_info['fields'].items(): 156 | mp.addpart(name=key, data=value) 157 | mp.addpart(name='file', content_type=file_type, filename=filename, data=file) 158 | 159 | upload_resp = self.session.post(file_upload_info['s3_bucket_url'], multipart=mp) 160 | 161 | if not upload_resp.ok: 162 | raise Exception('File upload error', upload_resp) 163 | 164 | # Extract the uploaded file URL 165 | if 'image/upload' in file_upload_info['s3_object_url']: 166 | uploaded_url = re.sub( 167 | r'/private/s--.*?--/v\\d+/user_uploads/', 168 | '/private/user_uploads/', 169 | upload_resp.json()['secure_url'] 170 | ) 171 | else: 172 | uploaded_url = file_upload_info['s3_object_url'] 173 | 174 | uploaded_files.append(uploaded_url) 175 | 176 | # Prepare the JSON payload for the query 177 | json_data = { 178 | 'query_str': query, 179 | 'params': { 180 | 'attachments': uploaded_files + follow_up['attachments'] if follow_up else uploaded_files, 181 | 'frontend_context_uuid': str(uuid4()), 182 | 'frontend_uuid': str(uuid4()), 183 | 'is_incognito': incognito, 184 | 'language': language, 185 | 'last_backend_uuid': follow_up['backend_uuid'] if follow_up else None, 186 | 'mode': 'concise' if mode == 'auto' else 'copilot', 187 | 'model_preference': { 188 | 'auto': { None: 'turbo' }, 189 | 'pro': { 190 | None: 'pplx_pro', 191 | 'sonar': 'experimental', 192 | 'gpt-4.5': 'gpt45', 193 | 'gpt-4o': 'gpt4o', 194 | 'claude 3.7 sonnet': 'claude2', 195 | 'gemini 2.0 flash': 'gemini2flash', 196 | 'grok-2': 'grok' 197 | }, 198 | 'reasoning': { 199 | None: 'pplx_reasoning', 200 | 'r1': 'r1', 201 | 'o3-mini': 'o3mini', 202 | 'claude 3.7 sonnet': 'claude37sonnetthinking' 203 | }, 204 | 'deep research': { None: 'pplx_alpha' } 205 | }[mode][model], 206 | 'source': 'default', 207 | 'sources': sources, 208 | 'version': '2.18' 209 | } 210 | } 211 | 212 | # Send the query request and handle the response 213 | resp = self.session.post('https://www.perplexity.ai/rest/sse/perplexity_ask', json=json_data, stream=True) 214 | chunks = [] 215 | 216 | def stream_response(resp): 217 | ''' 218 | Generator for streaming responses. 219 | ''' 220 | for chunk in resp.iter_lines(delimiter=b'\\r\\n\\r\\n'): 221 | content = chunk.decode('utf-8') 222 | 223 | if content.startswith('event: message\\r\\n'): 224 | content_json = json.loads(content[len('event: message\\r\\ndata: '):]) 225 | content_json['text'] = json.loads(content_json['text']) 226 | 227 | chunks.append(content_json) 228 | yield chunks[-1] 229 | 230 | elif content.startswith('event: end_of_stream\\r\\n'): 231 | return 232 | 233 | if stream: 234 | return stream_response(resp) 235 | 236 | for chunk in resp.iter_lines(delimiter=b'\\r\\n\\r\\n'): 237 | content = chunk.decode('utf-8') 238 | 239 | if content.startswith('event: message\\r\\n'): 240 | content_json = json.loads(content[len('event: message\\r\\ndata: '):]) 241 | content_json['text'] = json.loads(content_json['text']) 242 | 243 | chunks.append(content_json) 244 | 245 | elif content.startswith('event: end_of_stream\\r\\n'): 246 | return chunks[-1] -------------------------------------------------------------------------------- /perplexity/driver.py: -------------------------------------------------------------------------------- 1 | # Importing necessary modules 2 | # re: Regular expressions for pattern matching 3 | # json: JSON parsing and serialization 4 | # time: Time-related functions 5 | # threading: For running background tasks 6 | # urllib.parse: URL parsing utilities 7 | # curl_cffi: HTTP requests 8 | # playwright.sync_api: Synchronous Playwright API for browser automation 9 | # patchright.sync_api: Synchronous Patchright API for undetected browser automation 10 | import re 11 | import json 12 | import time 13 | from threading import Thread 14 | from urllib.parse import unquote 15 | from curl_cffi import requests 16 | from playwright.sync_api import sync_playwright 17 | from patchright.sync_api import sync_playwright as sync_patchright 18 | from .emailnator import Emailnator 19 | 20 | class Driver: 21 | ''' 22 | A driver for automating account creation and usage in Perplexity AI via a web interface. 23 | ''' 24 | 25 | def __init__(self): 26 | # Regular expression for extracting sign-in links 27 | self.signin_regex = re.compile(r'"(https://www\\.perplexity\\.ai/api/auth/callback/email\\?callbackUrl=.*?)"') 28 | 29 | # Flags and state variables 30 | self.creating_new_account = False 31 | self.account_creator_running = False 32 | self.renewing_emailnator_cookies = False 33 | self.background_pages = [] # List of background browser pages 34 | self.perplexity_cookies = None # Cookies for Perplexity AI 35 | self.emailnator_cookies = None # Cookies for Emailnator 36 | 37 | def account_creator(self): 38 | ''' 39 | Background task for creating new accounts. 40 | ''' 41 | self.new_account_link = None 42 | 43 | while True: 44 | if not self.new_account_link: 45 | print('Creating new account') 46 | 47 | while True: 48 | try: 49 | # Initialize Emailnator client 50 | emailnator_cli = Emailnator(self.emailnator_cookies, {**self.emailnator_headers, 'x-xsrf-token': unquote(self.emailnator_cookies['XSRF-TOKEN'])}) 51 | 52 | # Send a POST request to initiate account creation 53 | resp = requests.post('https://www.perplexity.ai/api/auth/signin/email', data={ 54 | 'email': emailnator_cli.email, 55 | 'csrfToken': self.perplexity_cookies['next-auth.csrf-token'].split('%')[0], 56 | 'callbackUrl': 'https://www.perplexity.ai/', 57 | 'json': 'true'}, 58 | headers=self.perplexity_headers, 59 | cookies=self.perplexity_cookies 60 | ) 61 | 62 | # Check if the response is successful 63 | if resp.ok: 64 | new_msgs = emailnator_cli.reload(wait_for=lambda x: x['subject'] == 'Sign in to Perplexity', timeout=20) 65 | 66 | if new_msgs: 67 | msg = emailnator_cli.get(func=lambda x: x['subject'] == 'Sign in to Perplexity') 68 | self.new_account_link = self.signin_regex.search(emailnator_cli.open(msg['messageID'])).group(1) 69 | 70 | print('New account created\n') 71 | break 72 | 73 | except Exception as e: 74 | print('Account creation error', e) 75 | print('Renewing emailnator cookies') 76 | 77 | # Reset Emailnator cookies and wait for renewal 78 | self.emailnator_cookies = None 79 | self.renewing_emailnator_cookies = True 80 | 81 | while not self.emailnator_cookies: 82 | time.sleep(0.1) 83 | 84 | else: 85 | time.sleep(1) 86 | 87 | def intercept_request(self, route, request): 88 | ''' 89 | Intercepts browser requests to manage cookies and account creation. 90 | ''' 91 | if self.renewing_emailnator_cookies and request.url != 'https://www.emailnator.com/': 92 | self.page.goto('https://www.emailnator.com/') 93 | return 94 | 95 | if request.url == 'https://www.perplexity.ai/': 96 | response = route.fetch() 97 | 98 | # Extract cookies from the request 99 | cookies = {x.split('=')[0]: x.split('=')[1] for x in request.headers['cookie'].split('; ')} 100 | 101 | if not self.perplexity_cookies and 'What do you want to know?' in response.text() and 'next-auth.csrf-token' in cookies: 102 | self.perplexity_headers = request.headers 103 | self.perplexity_cookies = cookies 104 | 105 | route.fulfill(body=':)') 106 | 107 | # Open a new page for Emailnator 108 | self.background_pages.append(self.page) 109 | self.page = self.browser.new_page() 110 | self.page.route('**/*', self.intercept_request) 111 | self.page.goto('https://www.emailnator.com/') 112 | 113 | else: 114 | route.fulfill(response=response) 115 | 116 | elif request.url == 'https://www.emailnator.com/': 117 | request_will_interrupt = False 118 | 119 | if self.renewing_emailnator_cookies: 120 | request_will_interrupt = True 121 | self.renewing_emailnator_cookies = False 122 | 123 | response = route.fetch() 124 | 125 | # Extract cookies from the request 126 | cookies = {x.split('=')[0]: x.split('=')[1] for x in request.headers['cookie'].split('; ')} 127 | 128 | if not self.emailnator_cookies and 'Temporary Disposable Gmail | Temp Mail | Email Generator' in response.text() and 'XSRF-TOKEN' in cookies: 129 | self.emailnator_headers = request.headers 130 | self.emailnator_cookies = cookies 131 | 132 | route.fulfill(body=':)') 133 | 134 | if not self.account_creator_running: 135 | self.account_creator_running = True 136 | Thread(target=self.account_creator).start() 137 | 138 | if request_will_interrupt: 139 | self.page.goto('https://www.perplexity.ai/') 140 | return 141 | 142 | # Open a new page for Perplexity AI 143 | self.background_pages.append(self.page) 144 | self.page = self.browser.new_page() 145 | self.page.route('**/*', self.intercept_request) 146 | 147 | for page in self.background_pages: 148 | page.close() 149 | 150 | while not self.new_account_link: 151 | self.page.wait_for_timeout(1000) 152 | 153 | self.page.goto(self.new_account_link) 154 | self.page.goto('https://www.perplexity.ai/') 155 | self.new_account_link = None 156 | 157 | else: 158 | route.fulfill(response=response) 159 | 160 | elif '/rest/rate-limit' in request.url: 161 | route.continue_() 162 | gpt4_limit = request.response().json()['remaining'] 163 | 164 | if not self.creating_new_account and gpt4_limit == 0: 165 | self.creating_new_account = True 166 | self.page = self.browser.new_page() 167 | self.page.route('**/*', self.intercept_request) 168 | 169 | while not self.new_account_link: 170 | self.page.wait_for_timeout(1000) 171 | 172 | self.page.goto(self.new_account_link) 173 | self.page.goto('https://www.perplexity.ai/') 174 | self.new_account_link = None 175 | 176 | else: 177 | route.continue_() 178 | 179 | def run(self, chrome_data_dir, port=None): 180 | ''' 181 | Launches the browser and starts intercepting requests. 182 | 183 | Parameters: 184 | - chrome_data_dir: Path to the Chrome user data directory. 185 | - port: Port for remote debugging (optional). 186 | ''' 187 | with (sync_playwright() if port else sync_patchright()) as playwright: 188 | if port: 189 | # Connect to an existing Chrome instance 190 | self.browser = playwright.chromium.connect_over_cdp(f'http://localhost:{port}') 191 | else: 192 | # Launch a new Chrome instance 193 | self.browser = playwright.chromium.launch_persistent_context( 194 | user_data_dir=chrome_data_dir, 195 | channel="chrome", 196 | headless=False, 197 | no_viewport=True 198 | ) 199 | 200 | self.page = self.browser.contexts[0].new_page() if port else self.browser.new_page() 201 | self.background_pages.append(self.page) 202 | self.page.route('**/*', self.intercept_request) 203 | self.page.goto('https://www.perplexity.ai/') 204 | 205 | while True: 206 | try: 207 | self.page.context.pages[-1].wait_for_timeout(1000) 208 | except Exception: 209 | pass -------------------------------------------------------------------------------- /perplexity/emailnator.py: -------------------------------------------------------------------------------- 1 | # Importing necessary modules 2 | # time: Time-related functions for delays and timeouts 3 | # urllib.parse: URL parsing utilities 4 | # curl_cffi: HTTP requests 5 | from urllib.parse import unquote 6 | from curl_cffi import requests 7 | 8 | class Emailnator: 9 | ''' 10 | A client for interacting with the Emailnator service to generate disposable email addresses. 11 | ''' 12 | 13 | def __init__(self, cookies, headers={}, domain=False, plus=False, dot=False, google_mail=True): 14 | # Initialize inbox and advertisement inbox 15 | self.inbox = [] 16 | self.inbox_ads = [] 17 | 18 | # Set default headers if not provided 19 | if not headers: 20 | headers = { 21 | 'accept': 'application/json, text/plain, */*', 22 | 'accept-language': 'en-US,en;q=0.9', 23 | 'content-type': 'application/json', 24 | 'dnt': '1', 25 | 'origin': 'https://www.emailnator.com', 26 | 'priority': 'u=1, i', 27 | 'referer': 'https://www.emailnator.com/', 28 | 'sec-ch-ua': '"Not;A=Brand";v="24", "Chromium";v="128"', 29 | 'sec-ch-ua-arch': '"x86"', 30 | 'sec-ch-ua-bitness': '"64"', 31 | 'sec-ch-ua-full-version': '"128.0.6613.120"', 32 | 'sec-ch-ua-full-version-list': '"Not;A=Brand";v="24.0.0.0", "Chromium";v="128.0.6613.120"', 33 | 'sec-ch-ua-mobile': '?0', 34 | 'sec-ch-ua-model': '""', 35 | 'sec-ch-ua-platform': '"Windows"', 36 | 'sec-ch-ua-platform-version': '"19.0.0"', 37 | 'sec-fetch-dest': 'empty', 38 | 'sec-fetch-mode': 'cors', 39 | 'sec-fetch-site': 'same-origin', 40 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 41 | 'x-requested-with': 'XMLHttpRequest', 42 | 'x-xsrf-token': unquote(cookies['XSRF-TOKEN']), 43 | } 44 | 45 | # Initialize HTTP session 46 | self.s = requests.Session(headers=headers, cookies=cookies) 47 | 48 | # Prepare email generation options 49 | data = {'email': []} 50 | if domain: 51 | data['email'].append('domain') 52 | if plus: 53 | data['email'].append('plusGmail') 54 | if dot: 55 | data['email'].append('dotGmail') 56 | if google_mail: 57 | data['email'].append('googleMail') 58 | 59 | # Generate a new email address 60 | while True: 61 | resp = self.s.post('https://www.emailnator.com/generate-email', json=data).json() 62 | if 'email' in resp: 63 | break 64 | 65 | self.email = resp['email'][0] # Store the generated email address 66 | 67 | # Load initial inbox advertisements 68 | for ads in self.s.post('https://www.emailnator.com/message-list', json={'email': self.email}).json()['messageData']: 69 | self.inbox_ads.append(ads['messageID']) 70 | 71 | def reload(self, wait=False, retry=5, timeout=30, wait_for=None): 72 | ''' 73 | Reloads the inbox to fetch new messages. 74 | 75 | Parameters: 76 | - wait: Whether to wait for new messages. 77 | - retry: Retry interval in seconds. 78 | - timeout: Maximum wait time in seconds. 79 | - wait_for: A function to filter messages. 80 | 81 | Returns: 82 | - List of new messages. 83 | ''' 84 | self.new_msgs = [] 85 | start = time.time() 86 | wait_for_found = False 87 | 88 | while True: 89 | # Fetch messages from the inbox 90 | for msg in self.s.post('https://www.emailnator.com/message-list', json={'email': self.email}).json()['messageData']: 91 | if msg['messageID'] not in self.inbox_ads and msg not in self.inbox: 92 | self.new_msgs.append(msg) 93 | 94 | if wait_for and wait_for(msg): 95 | wait_for_found = True 96 | 97 | if (wait and not self.new_msgs) or wait_for: 98 | if wait_for_found: 99 | break 100 | 101 | if time.time() - start > timeout: 102 | return 103 | 104 | time.sleep(retry) 105 | else: 106 | break 107 | 108 | self.inbox += self.new_msgs # Update the inbox with new messages 109 | return self.new_msgs 110 | 111 | def open(self, msg_id): 112 | ''' 113 | Opens a specific message by its ID. 114 | 115 | Parameters: 116 | - msg_id: The ID of the message to open. 117 | 118 | Returns: 119 | - The content of the message. 120 | ''' 121 | return self.s.post('https://www.emailnator.com/message-list', json={'email': self.email, 'messageID': msg_id}).text 122 | 123 | def get(self, func, msgs=[]): 124 | ''' 125 | Retrieves a message that matches a given condition. 126 | 127 | Parameters: 128 | - func: A function to filter messages. 129 | - msgs: List of messages to search (default: inbox). 130 | 131 | Returns: 132 | - The first message that matches the condition. 133 | ''' 134 | for msg in (msgs if msgs else self.inbox): 135 | if func(msg): 136 | return msg -------------------------------------------------------------------------------- /perplexity/labs.py: -------------------------------------------------------------------------------- 1 | # Importing necessary modules 2 | # ssl: SSL/TLS support for secure connections 3 | # json: JSON parsing and serialization 4 | # time: Time-related functions for delays 5 | # socket: Low-level networking interface 6 | # random: Random number generation 7 | # threading: For running background tasks 8 | # curl_cffi: HTTP requests 9 | # websocket: WebSocket client for real-time communication 10 | import ssl 11 | import json 12 | import time 13 | import socket 14 | import random 15 | from threading import Thread 16 | from curl_cffi import requests 17 | from websocket import WebSocketApp 18 | 19 | class LabsClient: 20 | ''' 21 | A client for interacting with the Perplexity AI Labs API. 22 | ''' 23 | 24 | def __init__(self): 25 | # Initialize HTTP session with default headers 26 | self.session = requests.Session(headers={ 27 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 28 | 'accept-language': 'en-US,en;q=0.9', 29 | 'cache-control': 'max-age=0', 30 | 'dnt': '1', 31 | 'priority': 'u=0, i', 32 | 'sec-ch-ua': '"Not;A=Brand";v="24", "Chromium";v="128"', 33 | 'sec-ch-ua-arch': '"x86"', 34 | 'sec-ch-ua-bitness': '"64"', 35 | 'sec-ch-ua-full-version': '"128.0.6613.120"', 36 | 'sec-ch-ua-full-version-list': '"Not;A=Brand";v="24.0.0.0", "Chromium";v="128.0.6613.120"', 37 | 'sec-ch-ua-mobile': '?0', 38 | 'sec-ch-ua-model': '""', 39 | 'sec-ch-ua-platform': '"Windows"', 40 | 'sec-ch-ua-platform-version': '"19.0.0"', 41 | 'sec-fetch-dest': 'document', 42 | 'sec-fetch-mode': 'navigate', 43 | 'sec-fetch-site': 'same-origin', 44 | 'sec-fetch-user': '?1', 45 | 'upgrade-insecure-requests': '1', 46 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 47 | }) 48 | 49 | # Generate a unique timestamp for session identification 50 | self.timestamp = format(random.getrandbits(32), '08x') 51 | 52 | # Establish a session with the Perplexity Labs API 53 | self.sid = json.loads(self.session.get(f'https://www.perplexity.ai/socket.io/?EIO=4&transport=polling&t={self.timestamp}').text[1:])['sid'] 54 | self.last_answer = None # Store the last response from the API 55 | self.history = [] # Maintain a history of queries and responses 56 | 57 | # Authenticate the session 58 | assert self.session.post(f'https://www.perplexity.ai/socket.io/?EIO=4&transport=polling&t={self.timestamp}&sid={self.sid}', data='40{"jwt":"anonymous-ask-user"}').text == 'OK' 59 | 60 | # Set up a secure WebSocket connection 61 | context = ssl.create_default_context() 62 | context.minimum_version = ssl.TLSVersion.TLSv1_3 63 | self.sock = context.wrap_socket(socket.create_connection(('www.perplexity.ai', 443)), server_hostname='www.perplexity.ai') 64 | 65 | # Initialize WebSocket client 66 | self.ws = WebSocketApp( 67 | url=f'wss://www.perplexity.ai/socket.io/?EIO=4&transport=websocket&sid={self.sid}', 68 | header={'User-Agent': self.session.headers['User-Agent']}, 69 | cookie='; '.join([f'{key}={value}' for key, value in self.session.cookies.get_dict().items()]), 70 | on_open=lambda ws: (ws.send('2probe'), ws.send('5')), 71 | on_message=self._on_message, 72 | on_error=lambda ws, error: print(f'Websocket Error: {error}'), 73 | socket=self.sock 74 | ) 75 | 76 | # Run the WebSocket client in a separate thread 77 | Thread(target=self.ws.run_forever, daemon=True).start() 78 | 79 | # Wait until the WebSocket connection is established 80 | while not (self.ws.sock and self.ws.sock.connected): 81 | time.sleep(0.01) 82 | 83 | def _on_message(self, ws, message): 84 | ''' 85 | WebSocket message handler. 86 | ''' 87 | if message == '2': 88 | ws.send('3') # Respond to ping messages 89 | 90 | if message.startswith('42'): 91 | response = json.loads(message[2:])[1] 92 | 93 | if 'final' in response: 94 | self.last_answer = response 95 | 96 | def ask(self, query, model='r1-1776', stream=False): 97 | ''' 98 | Sends a query to the Perplexity Labs API. 99 | 100 | Parameters: 101 | - query: The query string. 102 | - model: The model to use for the query. 103 | - stream: Whether to stream the response. 104 | 105 | Returns: 106 | - The final response or a generator for streaming responses. 107 | ''' 108 | assert model in ['r1-1776', 'sonar-pro', 'sonar', 'sonar-reasoning-pro', 'sonar-reasoning'], 'Invalid model.' 109 | 110 | self.last_answer = None 111 | self.history.append({'role': 'user', 'content': query}) 112 | 113 | # Send the query via WebSocket 114 | self.ws.send('42' + json.dumps([ 115 | 'perplexity_labs', 116 | { 117 | 'messages': self.history, 118 | 'model': model, 119 | 'source': 'default', 120 | 'version': '2.18', 121 | } 122 | ])) 123 | 124 | def stream_response(): 125 | ''' 126 | Generator for streaming responses. 127 | ''' 128 | answer = None 129 | 130 | while True: 131 | if self.last_answer != answer: 132 | answer = self.last_answer 133 | yield answer 134 | 135 | if self.last_answer and self.last_answer.get('final'): 136 | answer = self.last_answer 137 | self.last_answer = None 138 | self.history.append({'role': 'assistant', 'content': answer['output'], 'priority': 0}) 139 | 140 | return 141 | 142 | time.sleep(0.01) 143 | 144 | if stream: 145 | return stream_response() 146 | 147 | while True: 148 | if self.last_answer and self.last_answer.get('final'): 149 | answer = self.last_answer 150 | self.last_answer = None 151 | self.history.append({'role': 'assistant', 'content': answer['output'], 'priority': 0}) 152 | 153 | return answer 154 | 155 | time.sleep(0.01) -------------------------------------------------------------------------------- /perplexity_async/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .emailnator import Emailnator 3 | from .labs import LabsClient 4 | 5 | __all__ = ['Client', 'Emailnator', 'LabsClient'] -------------------------------------------------------------------------------- /perplexity_async/client.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import json 4 | import random 5 | import asyncio 6 | import mimetypes 7 | from uuid import uuid4 8 | from curl_cffi import requests, CurlMime 9 | 10 | from .emailnator import Emailnator 11 | 12 | 13 | class AsyncMixin: 14 | def __init__(self, *args, **kwargs): 15 | self.__storedargs = args, kwargs 16 | self.async_initialized = False 17 | 18 | async def __ainit__(self, *args, **kwargs): 19 | pass 20 | 21 | async def __initobj(self): 22 | assert not self.async_initialized 23 | self.async_initialized = True 24 | 25 | # pass the parameters to __ainit__ that passed to __init__ 26 | await self.__ainit__(*self.__storedargs[0], **self.__storedargs[1]) 27 | return self 28 | 29 | def __await__(self): 30 | return self.__initobj().__await__() 31 | 32 | class Client(AsyncMixin): 33 | ''' 34 | A client for interacting with the Perplexity AI API. 35 | ''' 36 | async def __ainit__(self, cookies={}): 37 | self.session = requests.AsyncSession(headers={ 38 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 39 | 'accept-language': 'en-US,en;q=0.9', 40 | 'cache-control': 'max-age=0', 41 | 'dnt': '1', 42 | 'priority': 'u=0, i', 43 | 'sec-ch-ua': '"Not;A=Brand";v="24", "Chromium";v="128"', 44 | 'sec-ch-ua-arch': '"x86"', 45 | 'sec-ch-ua-bitness': '"64"', 46 | 'sec-ch-ua-full-version': '"128.0.6613.120"', 47 | 'sec-ch-ua-full-version-list': '"Not;A=Brand";v="24.0.0.0", "Chromium";v="128.0.6613.120"', 48 | 'sec-ch-ua-mobile': '?0', 49 | 'sec-ch-ua-model': '""', 50 | 'sec-ch-ua-platform': '"Windows"', 51 | 'sec-ch-ua-platform-version': '"19.0.0"', 52 | 'sec-fetch-dest': 'document', 53 | 'sec-fetch-mode': 'navigate', 54 | 'sec-fetch-site': 'same-origin', 55 | 'sec-fetch-user': '?1', 56 | 'upgrade-insecure-requests': '1', 57 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 58 | }, cookies=cookies, impersonate='chrome') 59 | self.own = bool(cookies) 60 | self.copilot = 0 if not cookies else float('inf') 61 | self.file_upload = 0 if not cookies else float('inf') 62 | self.signin_regex = re.compile(r'"(https://www\.perplexity\.ai/api/auth/callback/email\?callbackUrl=.*?)"') 63 | self.timestamp = format(random.getrandbits(32), '08x') 64 | await self.session.get('https://www.perplexity.ai/api/auth/session') 65 | 66 | async def create_account(self, cookies): 67 | ''' 68 | Function to create a new account 69 | ''' 70 | while True: 71 | try: 72 | emailnator_cli = await Emailnator(cookies) 73 | 74 | resp = await self.session.post('https://www.perplexity.ai/api/auth/signin/email', data={ 75 | 'email': emailnator_cli.email, 76 | 'csrfToken': self.session.cookies.get_dict()['next-auth.csrf-token'].split('%')[0], 77 | 'callbackUrl': 'https://www.perplexity.ai/', 78 | 'json': 'true' 79 | }) 80 | 81 | if resp.ok: 82 | new_msgs = await emailnator_cli.reload(wait_for=lambda x: x['subject'] == 'Sign in to Perplexity', timeout=20) 83 | 84 | if new_msgs: 85 | break 86 | else: 87 | print('Perplexity account creating error:', resp) 88 | 89 | except Exception: 90 | pass 91 | 92 | msg = emailnator_cli.get(func=lambda x: x['subject'] == 'Sign in to Perplexity') 93 | new_account_link = self.signin_regex.search(await emailnator_cli.open(msg['messageID'])).group(1) 94 | 95 | await self.session.get(new_account_link) 96 | 97 | self.copilot = 5 98 | self.file_upload = 10 99 | 100 | return True 101 | 102 | async def search(self, query, mode='auto', model=None, sources=['web'], files={}, stream=False, language='en-US', follow_up=None, incognito=False): 103 | ''' 104 | Query function 105 | ''' 106 | assert mode in ['auto', 'pro', 'reasoning', 'deep research'], 'Search modes -> ["auto", "pro", "reasoning", "deep research"]' 107 | assert model in { 108 | 'auto': [None], 109 | 'pro': [None, 'sonar', 'gpt-4.5', 'gpt-4o', 'claude 3.7 sonnet', 'gemini 2.0 flash', 'grok-2'], 110 | 'reasoning': [None, 'r1', 'o3-mini', 'claude 3.7 sonnet'], 111 | 'deep research': [None] 112 | }[mode] if self.own else True, '''Models for modes -> { 113 | 'auto': [None], 114 | 'pro': [None, 'sonar', 'gpt-4.5', 'gpt-4o', 'claude 3.7 sonnet', 'gemini 2.0 flash', 'grok-2'], 115 | 'reasoning': [None, 'r1', 'o3-mini', 'claude 3.7 sonnet'], 116 | 'deep research': [None] 117 | }''' 118 | assert all([source in ('web', 'scholar', 'social') for source in sources]), 'Sources -> ["web", "scholar", "social"]' 119 | assert self.copilot > 0 if mode in ['pro', 'reasoning', 'deep research'] else True, 'You have used all of your enhanced (pro) queries' 120 | assert self.file_upload - len(files) >= 0 if files else True, f'You have tried to upload {len(files)} files but you have {self.file_upload} file upload(s) remaining.' 121 | 122 | self.copilot = self.copilot - 1 if mode in ['pro', 'reasoning', 'deep research'] else self.copilot 123 | self.file_upload = self.file_upload - len(files) if files else self.file_upload 124 | 125 | uploaded_files = [] 126 | 127 | for filename, file in files.items(): 128 | file_type = mimetypes.guess_type(filename)[0] 129 | file_upload_info = (await self.session.post( 130 | 'https://www.perplexity.ai/rest/uploads/create_upload_url?version=2.18&source=default', 131 | json={ 132 | 'content_type': file_type, 133 | 'file_size': sys.getsizeof(file), 134 | 'filename': filename, 135 | 'force_image': False, 136 | 'source': 'default', 137 | } 138 | )).json() 139 | 140 | mp = CurlMime() 141 | for key, value in file_upload_info['fields'].items(): 142 | mp.addpart(name=key, data=value) 143 | mp.addpart(name='file', content_type=file_type, filename=filename, data=file) 144 | 145 | upload_resp = await self.session.post(file_upload_info['s3_bucket_url'], multipart=mp) 146 | 147 | if not upload_resp.ok: 148 | raise Exception('File upload error', upload_resp) 149 | 150 | if 'image/upload' in file_upload_info['s3_object_url']: 151 | uploaded_url = re.sub( 152 | r'/private/s--.*?--/v\d+/user_uploads/', 153 | '/private/user_uploads/', 154 | upload_resp.json()['secure_url'] 155 | ) 156 | else: 157 | uploaded_url = file_upload_info['s3_object_url'] 158 | 159 | uploaded_files.append(uploaded_url) 160 | 161 | json_data = { 162 | 'query_str': query, 163 | 'params': 164 | { 165 | 'attachments': uploaded_files + follow_up['attachments'] if follow_up else uploaded_files, 166 | 'frontend_context_uuid': str(uuid4()), 167 | 'frontend_uuid': str(uuid4()), 168 | 'is_incognito': incognito, 169 | 'language': language, 170 | 'last_backend_uuid': follow_up['backend_uuid'] if follow_up else None, 171 | 'mode': 'concise' if mode == 'auto' else 'copilot', 172 | 'model_preference': { 173 | 'auto': { 174 | None: 'turbo' 175 | }, 176 | 'pro': { 177 | None: 'pplx_pro', 178 | 'sonar': 'experimental', 179 | 'gpt-4.5': 'gpt45', 180 | 'gpt-4o': 'gpt4o', 181 | 'claude 3.7 sonnet': 'claude2', 182 | 'gemini 2.0 flash': 'gemini2flash', 183 | 'grok-2': 'grok' 184 | }, 185 | 'reasoning': { 186 | None: 'pplx_reasoning', 187 | 'r1': 'r1', 188 | 'o3-mini': 'o3mini', 189 | 'claude 3.7 sonnet': 'claude37sonnetthinking' 190 | }, 191 | 'deep research': { 192 | None: 'pplx_alpha' 193 | } 194 | }[mode][model], 195 | 'source': 'default', 196 | 'sources': sources, 197 | 'version': '2.18' 198 | } 199 | } 200 | 201 | resp = await self.session.post('https://www.perplexity.ai/rest/sse/perplexity_ask', json=json_data, stream=True) 202 | chunks = [] 203 | 204 | async def stream_response(resp): 205 | async for chunk in resp.aiter_lines(delimiter=b'\r\n\r\n'): 206 | content = chunk.decode('utf-8') 207 | 208 | if content.startswith('event: message\r\n'): 209 | content_json = json.loads(content[len('event: message\r\ndata: '):]) 210 | content_json['text'] = json.loads(content_json['text']) 211 | 212 | chunks.append(content_json) 213 | yield chunks[-1] 214 | 215 | elif content.startswith('event: end_of_stream\r\n'): 216 | return 217 | 218 | if stream: 219 | return stream_response(resp) 220 | 221 | async for chunk in resp.aiter_lines(delimiter=b'\r\n\r\n'): 222 | content = chunk.decode('utf-8') 223 | 224 | if content.startswith('event: message\r\n'): 225 | content_json = json.loads(content[len('event: message\r\ndata: '):]) 226 | content_json['text'] = json.loads(content_json['text']) 227 | 228 | chunks.append(content_json) 229 | 230 | elif content.startswith('event: end_of_stream\r\n'): 231 | return chunks[-1] 232 | -------------------------------------------------------------------------------- /perplexity_async/emailnator.py: -------------------------------------------------------------------------------- 1 | import time 2 | import asyncio 3 | from urllib.parse import unquote 4 | from curl_cffi import requests 5 | 6 | 7 | # https://dev.to/akarshan/asynchronous-python-magic-how-to-create-awaitable-constructors-with-asyncmixin-18j5 8 | # https://web.archive.org/web/20230915163459/https://dev.to/akarshan/asynchronous-python-magic-how-to-create-awaitable-constructors-with-asyncmixin-18j5 9 | class AsyncMixin: 10 | def __init__(self, *args, **kwargs): 11 | self.__storedargs = args, kwargs 12 | self.async_initialized = False 13 | 14 | async def __ainit__(self, *args, **kwargs): 15 | pass 16 | 17 | async def __initobj(self): 18 | assert not self.async_initialized 19 | self.async_initialized = True 20 | # pass the parameters to __ainit__ that passed to __init__ 21 | await self.__ainit__(*self.__storedargs[0], **self.__storedargs[1]) 22 | return self 23 | 24 | def __await__(self): 25 | return self.__initobj().__await__() 26 | 27 | class Emailnator(AsyncMixin): 28 | async def __ainit__(self, cookies, headers={}, domain=False, plus=False, dot=False, google_mail=True): 29 | self.inbox = [] 30 | self.inbox_ads = [] 31 | 32 | if not headers: 33 | headers = { 34 | 'accept': 'application/json, text/plain, */*', 35 | 'accept-language': 'en-US,en;q=0.9', 36 | 'content-type': 'application/json', 37 | 'dnt': '1', 38 | 'origin': 'https://www.emailnator.com', 39 | 'priority': 'u=1, i', 40 | 'referer': 'https://www.emailnator.com/', 41 | 'sec-ch-ua': '"Not;A=Brand";v="24", "Chromium";v="128"', 42 | 'sec-ch-ua-arch': '"x86"', 43 | 'sec-ch-ua-bitness': '"64"', 44 | 'sec-ch-ua-full-version': '"128.0.6613.120"', 45 | 'sec-ch-ua-full-version-list': '"Not;A=Brand";v="24.0.0.0", "Chromium";v="128.0.6613.120"', 46 | 'sec-ch-ua-mobile': '?0', 47 | 'sec-ch-ua-model': '""', 48 | 'sec-ch-ua-platform': '"Windows"', 49 | 'sec-ch-ua-platform-version': '"19.0.0"', 50 | 'sec-fetch-dest': 'empty', 51 | 'sec-fetch-mode': 'cors', 52 | 'sec-fetch-site': 'same-origin', 53 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 54 | 'x-requested-with': 'XMLHttpRequest', 55 | 'x-xsrf-token': unquote(cookies['XSRF-TOKEN']), 56 | } 57 | 58 | self.s = requests.AsyncSession(headers=headers, cookies=cookies, impersonate='chrome') 59 | 60 | data = {'email': []} 61 | 62 | if domain: 63 | data['email'].append('domain') 64 | if plus: 65 | data['email'].append('plusGmail') 66 | if dot: 67 | data['email'].append('dotGmail') 68 | if google_mail: 69 | data['email'].append('googleMail') 70 | 71 | while True: 72 | resp = (await self.s.post('https://www.emailnator.com/generate-email', json=data)).json() 73 | 74 | if 'email' in resp: 75 | break 76 | 77 | self.email = resp['email'][0] 78 | 79 | for ads in (await self.s.post('https://www.emailnator.com/message-list', json={'email': self.email})).json()['messageData']: 80 | self.inbox_ads.append(ads['messageID']) 81 | 82 | async def reload(self, wait=False, retry=5, timeout=30, wait_for=None): 83 | self.new_msgs = [] 84 | start = time.time() 85 | wait_for_found = False 86 | 87 | while True: 88 | for msg in (await self.s.post('https://www.emailnator.com/message-list', json={'email': self.email})).json()['messageData']: 89 | if msg['messageID'] not in self.inbox_ads and msg not in self.inbox: 90 | self.new_msgs.append(msg) 91 | 92 | if wait_for(msg): 93 | wait_for_found = True 94 | 95 | if (wait and not self.new_msgs) or wait_for: 96 | if wait_for_found: 97 | break 98 | 99 | if time.time() - start > timeout: 100 | return 101 | 102 | await asyncio.sleep(retry) 103 | else: 104 | break 105 | 106 | self.inbox += self.new_msgs 107 | return self.new_msgs 108 | 109 | async def open(self, msg_id): 110 | return (await self.s.post('https://www.emailnator.com/message-list', json={'email': self.email, 'messageID': msg_id})).text 111 | 112 | def get(self, func, msgs=[]): 113 | for msg in (msgs if msgs else self.inbox): 114 | if func(msg): 115 | return msg -------------------------------------------------------------------------------- /perplexity_async/labs.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | import json 3 | import socket 4 | import random 5 | import asyncio 6 | from threading import Thread 7 | from curl_cffi import requests 8 | from websocket import WebSocketApp, WebSocketException 9 | 10 | 11 | class AsyncMixin: 12 | def __init__(self, *args, **kwargs): 13 | self.__storedargs = args, kwargs 14 | self.async_initialized = False 15 | 16 | async def __ainit__(self, *args, **kwargs): 17 | pass 18 | 19 | async def __initobj(self): 20 | assert not self.async_initialized 21 | self.async_initialized = True 22 | # pass the parameters to __ainit__ that passed to __init__ 23 | await self.__ainit__(*self.__storedargs[0], **self.__storedargs[1]) 24 | return self 25 | 26 | def __await__(self): 27 | return self.__initobj().__await__() 28 | 29 | class LabsClient(AsyncMixin): 30 | ''' 31 | A client for interacting with the Perplexity AI Labs API. 32 | ''' 33 | async def __ainit__(self): 34 | try: 35 | self.session = requests.AsyncSession(headers={ 36 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 37 | 'accept-language': 'en-US,en;q=0.9', 38 | 'cache-control': 'max-age=0', 39 | 'dnt': '1', 40 | 'priority': 'u=0, i', 41 | 'sec-ch-ua': '"Not;A=Brand";v="24", "Chromium";v="128"', 42 | 'sec-ch-ua-arch': '"x86"', 43 | 'sec-ch-ua-bitness': '"64"', 44 | 'sec-ch-ua-full-version': '"128.0.6613.120"', 45 | 'sec-ch-ua-full-version-list': '"Not;A=Brand";v="24.0.0.0", "Chromium";v="128.0.6613.120"', 46 | 'sec-ch-ua-mobile': '?0', 47 | 'sec-ch-ua-model': '""', 48 | 'sec-ch-ua-platform': '"Windows"', 49 | 'sec-ch-ua-platform-version': '"19.0.0"', 50 | 'sec-fetch-dest': 'document', 51 | 'sec-fetch-mode': 'navigate', 52 | 'sec-fetch-site': 'same-origin', 53 | 'sec-fetch-user': '?1', 54 | 'upgrade-insecure-requests': '1', 55 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 56 | }, impersonate='chrome') 57 | self.timestamp = format(random.getrandbits(32), '08x') 58 | response = await self.session.get(f'https://www.perplexity.ai/socket.io/?EIO=4&transport=polling&t={self.timestamp}') 59 | response.raise_for_status() 60 | self.sid = json.loads(response.text[1:])['sid'] 61 | self.last_answer = None 62 | self.history = [] 63 | 64 | post_response = await self.session.post(f'https://www.perplexity.ai/socket.io/?EIO=4&transport=polling&t={self.timestamp}&sid={self.sid}', data='40{"jwt":"anonymous-ask-user"}') 65 | post_response.raise_for_status() 66 | assert post_response.text == 'OK' 67 | 68 | context = ssl.create_default_context() 69 | context.minimum_version = ssl.TLSVersion.TLSv1_3 70 | self.sock = context.wrap_socket(socket.create_connection(('www.perplexity.ai', 443)), server_hostname='www.perplexity.ai') 71 | 72 | self.ws = WebSocketApp( 73 | url=f'wss://www.perplexity.ai/socket.io/?EIO=4&transport=websocket&sid={self.sid}', 74 | header={'User-Agent': self.session.headers['User-Agent']}, 75 | cookie='; '.join([f'{key}={value}' for key, value in self.session.cookies.get_dict().items()]), 76 | on_open=lambda ws: (ws.send('2probe'), ws.send('5')), 77 | on_message=self._on_message, 78 | on_error=self._on_error, 79 | socket=self.sock 80 | ) 81 | 82 | Thread(target=self.ws.run_forever, daemon=True).start() 83 | 84 | while not (self.ws.sock and self.ws.sock.connected): 85 | await asyncio.sleep(0.01) 86 | except (requests.RequestException, WebSocketException, socket.error, ssl.SSLError) as e: 87 | print(f"Initialization error: {e}") 88 | except Exception as e: 89 | print(f"Unexpected error during initialization: {e}") 90 | 91 | def _on_message(self, ws, message): 92 | ''' 93 | Websocket message handler 94 | ''' 95 | try: 96 | if message == '2': 97 | ws.send('3') 98 | 99 | if message.startswith('42'): 100 | response = json.loads(message[2:])[1] 101 | 102 | if 'final' in response: 103 | self.last_answer = response 104 | except json.JSONDecodeError as e: 105 | print(f"JSON decode error: {e}") 106 | except Exception as e: 107 | print(f"Unexpected error in message handler: {e}") 108 | 109 | def _on_error(self, ws, error): 110 | ''' 111 | Websocket error handler 112 | ''' 113 | print(f'Websocket Error: {error}') 114 | 115 | async def ask(self, query, model='r1-1776', stream=False): 116 | ''' 117 | Query function 118 | ''' 119 | try: 120 | assert model in ['r1-1776', 'sonar-pro', 'sonar', 'sonar-reasoning-pro', 'sonar-reasoning'], 'Search models -> ["r1-1776", "sonar-pro", "sonar", "sonar-reasoning-pro", "sonar-reasoning"]' 121 | 122 | self.last_answer = None 123 | self.history.append({'role': 'user', 'content': query}) 124 | 125 | self.ws.send('42' + json.dumps([ 126 | 'perplexity_labs', 127 | { 128 | 'messages': self.history, 129 | 'model': model, 130 | 'source': 'default', 131 | 'version': '2.18', 132 | } 133 | ])) 134 | 135 | async def stream_response(self): 136 | answer = None 137 | 138 | while True: 139 | if self.last_answer != answer: 140 | answer = self.last_answer 141 | yield answer 142 | 143 | if self.last_answer and self.last_answer.get('final'): 144 | answer = self.last_answer 145 | self.last_answer = None 146 | self.history.append({'role': 'assistant', 'content': answer['output'], 'priority': 0}) 147 | 148 | return 149 | 150 | await asyncio.sleep(0.01) 151 | 152 | while True: 153 | if self.last_answer and stream: 154 | return stream_response(self) 155 | 156 | elif self.last_answer and self.last_answer.get('final'): 157 | answer = self.last_answer 158 | self.last_answer = None 159 | self.history.append({'role': 'assistant', 'content': answer['output'], 'priority': 0}) 160 | 161 | return answer 162 | 163 | await asyncio.sleep(0.01) 164 | except AssertionError as e: 165 | print(f"Assertion error: {e}") 166 | except WebSocketException as e: 167 | print(f"WebSocket error: {e}") 168 | except Exception as e: 169 | print(f"Unexpected error in ask method: {e}") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | curl_cffi 2 | --------------------------------------------------------------------------------