├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── classes └── openai.py ├── config.json.example ├── requirements.txt └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | auth.json 3 | start.sh 4 | log.txt* 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "PyChatGPT"] 2 | path = PyChatGPT 3 | url = https://github.com/rawandahmad698/PyChatGPT.git 4 | ignore = dirty 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 史业民 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 | # ChatGPT-MS 2 | This repo is named by ChatGPT for Multi-Session ChatGPT API. It is developed to provide APIs for chatbots of WeChat, Dingding, Feishu, Discord, Slack, etc. 3 | 4 | The main code is copied from [PyChatGPT](https://github.com/rawandahmad698/PyChatGPT). 5 | 6 | ## Multi-Session ChatGPT (`Generated by ChatGPT`) 7 | There are several reasons why separated sessions are better than having everyone in one session when using a language model like GPT-3. 8 | 9 | First, having separate sessions allows for more personalized and focused conversations. In a single session with multiple people, the conversation can quickly become chaotic and difficult to follow. With separate sessions, each person can have their own dedicated conversation with the language model, allowing for more focused and coherent discussions. 10 | 11 | Second, separate sessions can improve the performance of the language model. In a single session with multiple people, the language model may have difficulty understanding who is speaking and what they are talking about. With separate sessions, the language model can focus on a single speaker at a time, which can improve its ability to generate relevant and appropriate responses. 12 | 13 | Third, separate sessions can improve the overall user experience. In a single session, it can be difficult for users to keep track of the conversation and understand who is speaking. With separate sessions, each user can have their own dedicated space to engage with the language model, which can make the conversation more intuitive and enjoyable. 14 | 15 | Overall, separated sessions are generally considered to be better than having everyone in one session when using a language model like GPT-3. They can improve the performance of the model, provide a more personalized and focused conversation, and improve the overall user experience. 16 | 17 | # Disclaimer 18 | This is not an official OpenAI product. This is a personal project and is not affiliated with OpenAI in any way. Don't sue me. 19 | 20 | # Setup 21 | ## Install 22 | ``` 23 | git clone --recurse-submodules https://github.com/shiyemin/ChatGPT-MS 24 | pip install -r requirements.txt 25 | ``` 26 | 27 | ## Configuration 28 | `cp config.json.example config.json` 29 | * Update config.json 30 | 31 | ## Start API server 32 | `python server.py` 33 | 34 | ## Use API 35 | ``` 36 | curl -d '{"message":"Who are you?", "user": "shiyemin"}' -H "Content-Type: application/json" -X POST http://localhost:5000/chat 37 | ``` 38 | -------------------------------------------------------------------------------- /classes/openai.py: -------------------------------------------------------------------------------- 1 | # Builtins 2 | import os 3 | import json 4 | import time 5 | import uuid 6 | from typing import Tuple 7 | 8 | # Requests 9 | import requests 10 | 11 | # Fancy stuff 12 | import colorama 13 | from colorama import Fore 14 | 15 | colorama.init(autoreset=True) 16 | 17 | from PyChatGPT.src.pychatgpt.classes import openai as OpenAI 18 | 19 | 20 | def get_access_token(): 21 | """ 22 | Get the access token 23 | returns: 24 | str: The access token 25 | """ 26 | try: 27 | # Get path using os, it's in ./Classes/auth.json 28 | path = os.path.dirname(os.path.abspath(__file__)) 29 | path = os.path.join(path, "auth.json") 30 | 31 | with open(path, 'r') as f: 32 | creds = json.load(f) 33 | return creds['access_token'], creds['expires_at'] 34 | except FileNotFoundError: 35 | return None, None 36 | 37 | 38 | class LocalOpenAIAuth(OpenAI.Auth): 39 | 40 | @staticmethod 41 | def save_access_token(access_token: str, expiry: int or None = None): 42 | """ 43 | Save access_token and an hour from now on CHATGPT_ACCESS_TOKEN CHATGPT_ACCESS_TOKEN_EXPIRY environment variables 44 | :param expiry: 45 | :param access_token: 46 | :return: 47 | """ 48 | try: 49 | print(f"{Fore.GREEN}[OpenAI][9] {Fore.WHITE}Saving access token...") 50 | expiry = expiry or int(time.time()) + 3600 51 | 52 | # Get path using os, it's in ./classes/auth.json 53 | path = os.path.dirname(os.path.abspath(__file__)) 54 | path = os.path.join(path, "auth.json") 55 | with open(path, "w") as f: 56 | f.write(json.dumps({"access_token": access_token, "expires_at": expiry})) 57 | 58 | print(f"{Fore.GREEN}[OpenAI][8] {Fore.WHITE}Saved access token") 59 | except Exception as e: 60 | raise e 61 | 62 | 63 | def ask_stream( 64 | auth_token: Tuple, 65 | prompt: str, 66 | conversation_id: 67 | str or None, 68 | previous_convo_id: str or None, 69 | proxies: str or dict or None 70 | ) -> Tuple[str, str or None, str or None]: 71 | auth_token, expiry = auth_token 72 | 73 | headers = { 74 | 'Content-Type': 'application/json', 75 | 'Authorization': f'Bearer {auth_token}', 76 | 'Accept': 'text/event-stream', 77 | 'Referer': 'https://chat.openai.com/chat', 78 | 'Origin': 'https://chat.openai.com', 79 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15', 80 | 'X-OpenAI-Assistant-App-Id': '' 81 | } 82 | if previous_convo_id is None: 83 | previous_convo_id = str(uuid.uuid4()) 84 | 85 | data = { 86 | "action": "next", 87 | "messages": [ 88 | { 89 | "id": str(uuid.uuid4()), 90 | "role": "user", 91 | "content": {"content_type": "text", "parts": [prompt]} 92 | } 93 | ], 94 | "conversation_id": conversation_id, 95 | "parent_message_id": previous_convo_id, 96 | "model": "text-davinci-002-render" 97 | } 98 | response = requests.post("https://chat.openai.com/backend-api/conversation", 99 | headers=headers, data=json.dumps(data), stream=True, timeout=50) 100 | for line in response.iter_lines(): 101 | try: 102 | line = line.decode('utf-8') 103 | if line == "": 104 | continue 105 | line = line[6:] 106 | line = json.loads(line) 107 | try: 108 | message = line["message"]["content"]["parts"][0] 109 | previous_convo = line["message"]["id"] 110 | conversation_id = line["conversation_id"] 111 | except: 112 | continue 113 | yield message, previous_convo, conversation_id 114 | except: 115 | continue 116 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "email": "xxxx@email.com", 3 | "password": "PASSWORD" 4 | } 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | expiringdict 3 | requests 4 | bs4 5 | svglib 6 | colorama 7 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | """Make some requests to OpenAI's chatbot""" 2 | 3 | import os 4 | import time 5 | import json 6 | import base64 7 | import threading 8 | from expiringdict import ExpiringDict 9 | 10 | from flask import Flask, request, jsonify, Response 11 | 12 | from classes import openai as OpenAI 13 | from PyChatGPT.src.pychatgpt.classes import chat as Chat 14 | 15 | # Fancy stuff 16 | import colorama 17 | from colorama import Fore 18 | 19 | 20 | MAX_SESSION_NUM = 3000 21 | MAX_AGE_SECONDS = 1800 22 | 23 | APP = Flask(__name__) 24 | 25 | colorama.init(autoreset=True) 26 | 27 | # Check if config.json exists 28 | if not os.path.exists("config.json"): 29 | print(">> config.json is missing. Please create it.") 30 | print(f"{Fore.RED}>> Exiting...") 31 | exit(1) 32 | 33 | with open("config.json", "r") as f: 34 | config = json.load(f) 35 | # Check if email & password are in config.json 36 | if "email" not in config or "password" not in config: 37 | print(">> config.json is missing email or password. Please add them.") 38 | print(f"{Fore.RED}>> Exiting...") 39 | exit(1) 40 | 41 | # Get access token 42 | access_token = OpenAI.get_access_token() 43 | def access_token_expired(): 44 | if access_token is None or \ 45 | access_token[0] is None or \ 46 | access_token[1] is None or \ 47 | access_token[1] < time.time(): 48 | return True 49 | return False 50 | 51 | # Try login 52 | sem = threading.Semaphore() 53 | def try_login(): 54 | global access_token 55 | sem.acquire() 56 | if access_token_expired(): 57 | print(f"{Fore.RED}>> Try to refresh credentials.") 58 | open_ai_auth = OpenAI.LocalOpenAIAuth(email_address=config["email"], password=config["password"]) 59 | open_ai_auth.create_token() 60 | 61 | # If after creating the token, it's still expired, then something went wrong. 62 | access_token = OpenAI.get_access_token() 63 | if access_token_expired(): 64 | print(f"{Fore.RED}>> Failed to refresh credentials. Please try again.") 65 | exit(1) 66 | else: 67 | print(f"{Fore.GREEN}>> Successfully refreshed credentials.") 68 | 69 | sem.release() 70 | 71 | if access_token_expired(): 72 | try_login() 73 | else: 74 | print(f"{Fore.GREEN}>> Your credentials are valid.") 75 | 76 | # Cache all conv id 77 | # user => (conversation_id, previous_convo_id) 78 | prev_conv_id_cache = ExpiringDict(max_len=MAX_SESSION_NUM, max_age_seconds=MAX_AGE_SECONDS) 79 | def get_prev_conv_id(user): 80 | if user not in prev_conv_id_cache: 81 | prev_conv_id_cache[user] = (None, None) 82 | conversation_id, prev_conv_id = prev_conv_id_cache[user] 83 | return conversation_id, prev_conv_id 84 | 85 | def set_prev_conv_id(user, conversation_id, prev_conv_id): 86 | prev_conv_id_cache[user] = (conversation_id, prev_conv_id) 87 | 88 | 89 | @APP.route("/chat", methods=["POST"]) 90 | def chat(): 91 | global access_token 92 | 93 | message = request.json["message"].strip() 94 | user = request.json["user"].strip() 95 | 96 | if access_token_expired(): 97 | try: 98 | try_login() 99 | except: 100 | return Response("ChatGPT login error", status=400) 101 | 102 | print(f"{Fore.RED}[FROM {user}] >> {message}") 103 | if message == "reset": 104 | set_prev_conv_id(user, None, None) 105 | answer = "done" 106 | else: 107 | conversation_id, prev_conv_id = get_prev_conv_id(user) 108 | answer, previous_convo, convo_id = Chat.ask(auth_token=access_token, 109 | prompt=message, 110 | conversation_id=conversation_id, 111 | previous_convo_id=prev_conv_id, 112 | proxies=None) 113 | if answer == "400" or answer == "401": 114 | print(f"{Fore.RED}>> Failed to get a response from the API.") 115 | return Response( 116 | "Please try again latter.", 117 | status=400, 118 | ) 119 | set_prev_conv_id(user, convo_id, previous_convo) 120 | 121 | print(f"{Fore.GREEN}[TO {user}] >> {answer}") 122 | return jsonify({"response": answer}), 200 123 | 124 | 125 | def update_id_in_stream(user, **kwargs): 126 | answer = None 127 | try: 128 | for answer, previous_convo, convo_id in OpenAI.ask_stream(**kwargs): 129 | set_prev_conv_id(user, convo_id, previous_convo) 130 | yield json.dumps({"response": answer}) 131 | except GeneratorExit: 132 | pass 133 | if answer is None: 134 | answer = "Unknown error." 135 | yield json.dumps({"response": answer}) 136 | print(f"{Fore.GREEN}[TO {user}] >> {answer}") 137 | yield json.dumps({"response": "[DONE]"}) 138 | 139 | 140 | @APP.route("/chat-stream", methods=["POST"]) 141 | def chat_stream(): 142 | global access_token 143 | 144 | message = request.json["message"].strip() 145 | user = request.json["user"].strip() 146 | 147 | if access_token_expired(): 148 | try: 149 | try_login() 150 | except: 151 | return Response("ChatGPT login error", status=400) 152 | 153 | print(f"{Fore.RED}[FROM {user}] >> {message}") 154 | if message == "reset": 155 | set_prev_conv_id(user, None, None) 156 | return jsonify({"response": "done"}), 200 157 | else: 158 | conversation_id, prev_conv_id = get_prev_conv_id(user) 159 | return Response(update_id_in_stream(user=user, 160 | auth_token=access_token, 161 | prompt=message, 162 | conversation_id=conversation_id, 163 | previous_convo_id=prev_conv_id, 164 | proxies=None)) 165 | 166 | def start_browser(): 167 | APP.run(port=5000, threaded=True) 168 | 169 | if __name__ == "__main__": 170 | start_browser() 171 | --------------------------------------------------------------------------------