diff --git a/.env.example b/.env.example index c791666..8412f84 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,6 @@ USERNAME="@chatgpt" OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" BING_API_ENDPOINT="http://api:3000/conversation" BARD_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx." -BING_AUTH_COOKIE="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ No newline at end of file +BING_AUTH_COOKIE="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +PANDORA_API_ENDPOINT="http://127.0.0.1:8008" +PANDORA_API_MODEL="text-davinci-002-render-sha-mobile" \ No newline at end of file diff --git a/README.md b/README.md index 997c6cc..11efd9c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ ## Introduction -This is a simple Mattermost Bot that uses OpenAI's GPT API and Bing AI and Google Bard to generate responses to user inputs. The bot responds to six types of prompts: `!gpt`, `!chat` and `!bing` and `!pic` and `!bard` and `!help` depending on the first word of the prompt. +This is a simple Mattermost Bot that uses OpenAI's GPT API and Bing AI and Google Bard to generate responses to user inputs. The bot responds to these commands: `!gpt`, `!chat` and `!bing` and `!pic` and `!bard` and `!talk` and `!goon` and `!new` and `!help` depending on the first word of the prompt. ## Feature -1. Support Openai ChatGPT and Bing AI and Google Bard(US only at the moment) +1. Support Openai ChatGPT and Bing AI and Google Bard 2. Support Bing Image Creator +3. [pandora](https://github.com/pengzhile/pandora) ## Installation and Setup @@ -17,6 +18,20 @@ Edit `config.json` or `.env` with proper values docker compose up -d ``` +## Commands + +- `!help` help message +- `!gpt + [prompt]` generate a one time response from chatGPT +- `!chat + [prompt]` chat using official chatGPT api with context conversation +- `!bing + [prompt]` chat with Bing AI with context conversation +- `!bard + [prompt]` chat with Google's Bard +- `!pic + [prompt]` generate an image from Bing Image Creator + +The following commands need pandora http api: https://github.com/pengzhile/pandora/blob/master/doc/wiki_en.md#http-restful-api +- `!talk + [prompt]` chat using chatGPT web with context conversation +- `!goon` ask chatGPT to complete the missing part from previous conversation +- `!new` start a new converstaion + ## Demo ![demo1](https://i.imgur.com/XRAQB4B.jpg) diff --git a/bot.py b/bot.py index 65af381..37fabb7 100644 --- a/bot.py +++ b/bot.py @@ -11,6 +11,8 @@ from bing import BingBot from bard import Bardbot from BingImageGen import ImageGenAsync from log import getlogger +from pandora import Pandora +import uuid logger = getlogger() @@ -26,6 +28,8 @@ class Bot: openai_api_key: Optional[str] = None, openai_api_endpoint: Optional[str] = None, bing_api_endpoint: Optional[str] = None, + pandora_api_endpoint: Optional[str] = None, + pandora_api_model: Optional[str] = None, bard_token: Optional[str] = None, bing_auth_cookie: Optional[str] = None, port: int = 443, @@ -112,6 +116,18 @@ class Bot: "bing_api_endpoint is not provided, !bing command will not work" ) + # initialize pandora + if pandora_api_endpoint is not None: + self.pandora_api_endpoint = pandora_api_endpoint + self.pandora = Pandora( + api_endpoint=pandora_api_endpoint + ) + self.pandora_init() + if pandora_api_model is None: + self.pandora_api_model = "text-davinci-002-render-sha-mobile" + else: + self.pandora_api_model = pandora_api_model + self.bard_token = bard_token # initialize bard if self.bard_token is not None: @@ -128,24 +144,35 @@ class Bot: "bing_auth_cookie is not provided, !pic command will not work" ) - # regular expression to match keyword [!gpt {prompt}] [!chat {prompt}] [!bing {prompt}] [!pic {prompt}] [!bard {prompt}] + # regular expression to match keyword self.gpt_prog = re.compile(r"^\s*!gpt\s*(.+)$") self.chat_prog = re.compile(r"^\s*!chat\s*(.+)$") self.bing_prog = re.compile(r"^\s*!bing\s*(.+)$") self.bard_prog = re.compile(r"^\s*!bard\s*(.+)$") self.pic_prog = re.compile(r"^\s*!pic\s*(.+)$") self.help_prog = re.compile(r"^\s*!help\s*.*$") + self.talk_prog = re.compile(r"^\s*!talk\s*(.+)$") + self.goon_prog = re.compile(r"^\s*!goon\s*.*$") + self.new_prog = re.compile(r"^\s*!new\s*.*$") # close session def __del__(self) -> None: self.driver.disconnect() + async def __aenter__(self): + return self + async def __aexit__(self, exc_type, exc_val, exc_tb): await self.session.close() def login(self) -> None: self.driver.login() + def pandora_init(self) -> None: + self.conversation_id = None + self.parent_message_id = str(uuid.uuid4()) + self.first_time = True + async def run(self) -> None: await self.driver.init_websocket(self.websocket_handler) @@ -162,6 +189,7 @@ class Bot: channel_id = raw_data_dict["channel_id"] sender_name = response["data"]["sender_name"] raw_message = raw_data_dict["message"] + try: asyncio.create_task( self.message_callback( @@ -217,6 +245,69 @@ class Bot: logger.error(e, exc_info=True) raise Exception(e) + if self.pandora_api_endpoint is not None: + # !talk command trigger handler + if self.talk_prog.match(message): + prompt = self.talk_prog.match(message).group(1) + try: + if self.conversation_id is not None: + data = { + "prompt": prompt, + "model": self.pandora_api_model, + "parent_message_id": self.parent_message_id, + "conversation_id": self.conversation_id, + "stream": False, + } + else: + data = { + "prompt": prompt, + "model": self.pandora_api_model, + "parent_message_id": self.parent_message_id, + "stream": False, + } + response = await self.pandora.talk(data) + self.conversation_id = response['conversation_id'] + self.parent_message_id = response['message']['id'] + content = response['message']['content']['parts'][0] + if self.first_time: + self.first_time = False + data = { + "model": self.pandora_api_model, + "message_id": self.parent_message_id, + } + await self.pandora.gen_title(data, self.conversation_id) + + await asyncio.to_thread( + self.send_message, channel_id, f"{content}" + ) + except Exception as e: + logger.error(e, exc_info=True) + raise Exception(e) + + # !goon command trigger handler + if self.goon_prog.match(message) and self.conversation_id is not None: + try: + data = { + "model": self.pandora_api_model, + "parent_message_id": self.parent_message_id, + "conversation_id": self.conversation_id, + "stream": False, + } + response = await self.pandora.goon(data) + self.conversation_id = response['conversation_id'] + self.parent_message_id = response['message']['id'] + content = response['message']['content']['parts'][0] + await asyncio.to_thread( + self.send_message, channel_id, f"{content}" + ) + except Exception as e: + logger.error(e, exc_info=True) + raise Exception(e) + + # !new command trigger handler + if self.new_prog.match(message): + self.pandora_init() + if self.bard_token is not None: # !bard command trigger handler if self.bard_prog.match(message): @@ -265,7 +356,7 @@ class Bot: self.driver.posts.create_post( options={ "channel_id": channel_id, - "message": message, + "message": message } ) @@ -321,6 +412,9 @@ class Bot: + "!bing [content], chat with context conversation powered by Bing AI\n" + "!bard [content], chat with Google's Bard\n" + "!pic [prompt], Image generation by Microsoft Bing\n" + + "!talk [content], talk using chatgpt web\n" + + "!goon, continue the incomplete conversation\n" + + "!new, start a new conversation\n" + "!help, help message" ) return help_info diff --git a/config.json.example b/config.json.example index 02c0558..a391781 100644 --- a/config.json.example +++ b/config.json.example @@ -5,5 +5,7 @@ "openai_api_key": "sk-xxxxxxxxxxxxxxxxxxx", "bing_api_endpoint": "http://api:3000/conversation", "bard_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx.", - "bing_auth_cookie": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + "bing_auth_cookie": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "pandora_api_endpoint": "http://127.0.0.1:8008", + "pandora_api_model": "text-davinci-002-render-sha-mobile" } \ No newline at end of file diff --git a/main.py b/main.py index 1ac5123..e85f0b7 100644 --- a/main.py +++ b/main.py @@ -20,6 +20,8 @@ async def main(): bing_api_endpoint=config.get("bing_api_endpoint"), bard_token=config.get("bard_token"), bing_auth_cookie=config.get("bing_auth_cookie"), + pandora_api_endpoint=config.get("pandora_api_endpoint"), + pandora_api_model=config.get("pandora_api_model"), port=config.get("port"), timeout=config.get("timeout"), ) @@ -36,6 +38,8 @@ async def main(): bing_api_endpoint=os.environ.get("BING_API_ENDPOINT"), bard_token=os.environ.get("BARD_TOKEN"), bing_auth_cookie=os.environ.get("BING_AUTH_COOKIE"), + pandora_api_endpoint=os.environ.get("PANDORA_API_ENDPOINT"), + pandora_api_model=os.environ.get("PANDORA_API_MODEL"), port=os.environ.get("PORT"), timeout=os.environ.get("TIMEOUT"), ) diff --git a/pandora.py b/pandora.py new file mode 100644 index 0000000..59af886 --- /dev/null +++ b/pandora.py @@ -0,0 +1,100 @@ +# https://github.com/pengzhile/pandora/blob/master/doc/HTTP-API.md +import uuid +import aiohttp +import asyncio +class Pandora: + def __init__(self, api_endpoint: str) -> None: + self.api_endpoint = api_endpoint.rstrip('/') + self.session = aiohttp.ClientSession() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.session.close() + + async def gen_title(self, data: dict, conversation_id: str) -> None: + """ + data = { + "model": "", + "message_id": "", + } + :param data: dict + :param conversation_id: str + :return: None + """ + api_endpoint = self.api_endpoint + f"/api/conversation/gen_title/{conversation_id}" + async with self.session.post(api_endpoint, json=data) as resp: + return await resp.json() + + async def talk(self, data: dict) -> None: + api_endpoint = self.api_endpoint + "/api/conversation/talk" + """ + data = { + "prompt": "", + "model": "", + "parent_message_id": "", + "conversation_id": "", # ignore at the first time + "stream": True, + } + :param data: dict + :return: None + """ + data['message_id'] = str(uuid.uuid4()) + async with self.session.post(api_endpoint, json=data) as resp: + return await resp.json() + + async def goon(self, data: dict) -> None: + """ + data = { + "model": "", + "parent_message_id": "", + "conversation_id": "", + "stream": True, + } + """ + api_endpoint = self.api_endpoint + "/api/conversation/goon" + async with self.session.post(api_endpoint, json=data) as resp: + return await resp.json() + +async def test(): + model = "text-davinci-002-render-sha-mobile" + api_endpoint = "http://127.0.0.1:8008" + client = Pandora(api_endpoint) + conversation_id = None + parent_message_id = str(uuid.uuid4()) + first_time = True + async with client: + while True: + prompt = input("BobMaster: ") + if conversation_id: + data = { + "prompt": prompt, + "model": model, + "parent_message_id": parent_message_id, + "conversation_id": conversation_id, + "stream": False, + } + else: + data = { + "prompt": prompt, + "model": model, + "parent_message_id": parent_message_id, + "stream": False, + } + response = await client.talk(data) + conversation_id = response['conversation_id'] + parent_message_id = response['message']['id'] + content = response['message']['content']['parts'][0] + print("ChatGPT: " + content + "\n") + if first_time: + first_time = False + data = { + "model": model, + "message_id": parent_message_id, + } + response = await client.gen_title(data, conversation_id) + + +if __name__ == '__main__': + asyncio.run(test()) \ No newline at end of file