diff --git a/.env.example b/.env.example index 3f48b16..a42e78d 100644 --- a/.env.example +++ b/.env.example @@ -15,4 +15,6 @@ OUTPUT_FOUR_IMAGES="true" # Optional IMPORT_KEYS_PATH="element-keys.txt" # Optional IMPORT_KEYS_PASSWORD="xxxxxxx" # Optional FLOWISE_API_URL="http://localhost:3000/api/v1/prediction/xxxx" # Optional -FLOWISE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxx" # Optional \ No newline at end of file +FLOWISE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxx" # Optional +PANDORA_API_ENDPOINT="http://pandora:8008" # Optional +PANDORA_API_MODEL="text-davinci-002-render-sha-mobile" # Optional \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index a6735e5..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.analysis.typeCheckingMode": "off" -} \ No newline at end of file diff --git a/README.md b/README.md index b02cf7d..9668a5f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ ## Introduction -This is a simple Matrix 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 `!lc` depending on the first word of the prompt. +This is a simple Matrix 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`, `!goon`, `!new` and `!lc` and `!help` depending on the first word of the prompt. ![Bing](https://user-images.githubusercontent.com/32976627/231073146-3e380217-a6a2-413d-9203-ab36965b909d.png) ![image](https://user-images.githubusercontent.com/32976627/232036790-e830145c-914e-40be-b3e6-c02cba93329c.png) ![ChatGPT](https://i.imgur.com/kK4rnPf.jpeg) ## Feature -1. Support Openai ChatGPT and Bing AI and Google Bard and Langchain([Flowise](https://github.com/FlowiseAI/Flowise)) +1. Support Openai ChatGPT and Bing AI and Google Bard 2. Support Bing Image Creator 3. Support E2E Encrypted Room 4. Colorful code blocks +5. Langchain([Flowise](https://github.com/FlowiseAI/Flowise)) +6. ChatGPT Web ([pandora](https://github.com/pengzhile/pandora) with Session isolation support) ## Installation and Setup @@ -109,6 +111,12 @@ To interact with the bot, simply send a message to the bot in the Matrix room wi !pic A bridal bouquet made of succulents ``` +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 + ## Bing AI and Image Generation diff --git a/bot.py b/bot.py index d68cc34..eb7b6b3 100644 --- a/bot.py +++ b/bot.py @@ -4,13 +4,27 @@ import re import sys import traceback from typing import Union, Optional +import uuid import aiohttp -from nio import (AsyncClient, AsyncClientConfig, InviteMemberEvent, JoinError, - KeyVerificationCancel, KeyVerificationEvent, EncryptionError, - KeyVerificationKey, KeyVerificationMac, KeyVerificationStart, - LocalProtocolError, LoginResponse, MatrixRoom, MegolmEvent, - RoomMessageText, ToDeviceError) +from nio import ( + AsyncClient, + AsyncClientConfig, + InviteMemberEvent, + JoinError, + KeyVerificationCancel, + KeyVerificationEvent, + EncryptionError, + KeyVerificationKey, + KeyVerificationMac, + KeyVerificationStart, + LocalProtocolError, + LoginResponse, + MatrixRoom, + MegolmEvent, + RoomMessageText, + ToDeviceError, +) from nio.store.database import SqliteStore from askgpt import askGPT @@ -22,6 +36,7 @@ from send_message import send_room_message from v3 import Chatbot from bard import Bardbot from flowise import flowise_query +from pandora import Pandora logger = getlogger() @@ -32,8 +47,8 @@ class Bot: homeserver: str, user_id: str, device_id: str, - chatgpt_api_endpoint: str = os.environ.get( - "CHATGPT_API_ENDPOINT") or "https://api.openai.com/v1/chat/completions", + chatgpt_api_endpoint: str = os.environ.get("CHATGPT_API_ENDPOINT") + or "https://api.openai.com/v1/chat/completions", api_key: Union[str, None] = None, room_id: Union[str, None] = None, bing_api_endpoint: Union[str, None] = None, @@ -41,20 +56,21 @@ class Bot: access_token: Union[str, None] = None, bard_token: Union[str, None] = None, jailbreakEnabled: Union[bool, None] = True, - bing_auth_cookie: Union[str, None] = '', + bing_auth_cookie: Union[str, None] = "", markdown_formatted: Union[bool, None] = False, output_four_images: Union[bool, None] = False, import_keys_path: Optional[str] = None, import_keys_password: Optional[str] = None, flowise_api_url: Optional[str] = None, - flowise_api_key: Optional[str] = None + flowise_api_key: Optional[str] = None, + pandora_api_endpoint: Optional[str] = None, + pandora_api_model: Optional[str] = None, ): - if (homeserver is None or user_id is None - or device_id is None): + if homeserver is None or user_id is None or device_id is None: logger.warning("homeserver && user_id && device_id is required") sys.exit(1) - if (password is None and access_token is None): + if password is None and access_token is None: logger.warning("password or access_toekn is required") sys.exit(1) @@ -75,7 +91,7 @@ class Bot: self.session = aiohttp.ClientSession() if bing_api_endpoint is None: - self.bing_api_endpoint = '' + self.bing_api_endpoint = "" else: self.bing_api_endpoint = bing_api_endpoint @@ -85,7 +101,7 @@ class Bot: self.jailbreakEnabled = jailbreakEnabled if bing_auth_cookie is None: - self.bing_auth_cookie = '' + self.bing_auth_cookie = "" else: self.bing_auth_cookie = bing_auth_cookie @@ -101,27 +117,30 @@ class Bot: # initialize AsyncClient object self.store_path = os.getcwd() - self.config = AsyncClientConfig(store=SqliteStore, - store_name="db", - store_sync_tokens=True, - encryption_enabled=True, - ) - self.client = AsyncClient(homeserver=self.homeserver, user=self.user_id, - device_id=self.device_id, - config=self.config, store_path=self.store_path,) + self.config = AsyncClientConfig( + store=SqliteStore, + store_name="db", + store_sync_tokens=True, + encryption_enabled=True, + ) + self.client = AsyncClient( + homeserver=self.homeserver, + user=self.user_id, + device_id=self.device_id, + config=self.config, + store_path=self.store_path, + ) if self.access_token is not None: self.client.access_token = self.access_token # setup event callbacks - self.client.add_event_callback( - self.message_callback, (RoomMessageText, )) - self.client.add_event_callback( - self.decryption_failure, (MegolmEvent, )) - self.client.add_event_callback( - self.invite_callback, (InviteMemberEvent, )) + self.client.add_event_callback(self.message_callback, (RoomMessageText,)) + self.client.add_event_callback(self.decryption_failure, (MegolmEvent,)) + self.client.add_event_callback(self.invite_callback, (InviteMemberEvent,)) self.client.add_to_device_callback( - self.to_device_callback, (KeyVerificationEvent, )) + self.to_device_callback, (KeyVerificationEvent,) + ) # regular expression to match keyword commands self.gpt_prog = re.compile(r"^\s*!gpt\s*(.+)$") @@ -131,6 +150,9 @@ class Bot: self.pic_prog = re.compile(r"^\s*!pic\s*(.+)$") self.lc_prog = re.compile(r"^\s*!lc\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*.*$") # initialize chatbot and chatgpt_api_endpoint if self.api_key is not None: @@ -147,18 +169,32 @@ class Bot: self.askgpt = askGPT(self.session) # initialize bingbot - if self.bing_api_endpoint != '': + if self.bing_api_endpoint != "": self.bingbot = BingBot( - self.session, bing_api_endpoint, jailbreakEnabled=self.jailbreakEnabled) + self.session, bing_api_endpoint, jailbreakEnabled=self.jailbreakEnabled + ) # initialize BingImageGenAsync - if self.bing_auth_cookie != '': + if self.bing_auth_cookie != "": self.imageGen = ImageGenAsync(self.bing_auth_cookie, quiet=True) # initialize Bardbot if bard_token is not None: self.bardbot = Bardbot(self.bard_token) + # initialize pandora + if pandora_api_endpoint is not None: + self.pandora_api_endpoint = pandora_api_endpoint + self.pandora = Pandora( + api_endpoint=pandora_api_endpoint, clientSession=self.session + ) + 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.pandora_data = {} + def __del__(self): try: loop = asyncio.get_running_loop() @@ -170,6 +206,13 @@ class Bot: async def _close(self): await self.session.close() + def pandora_init(self, sender_id: str) -> None: + self.pandora_data[sender_id] = { + "conversation_id": None, + "parent_message_id": str(uuid.uuid4()), + "first_time": True, + } + # message_callback RoomMessageText event async def message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None: if self.room_id is None: @@ -186,6 +229,9 @@ class Bot: # sender_id sender_id = event.sender + if sender_id not in self.pandora_data: + self.pandora_init(sender_id) + # user_message raw_user_message = event.body @@ -206,59 +252,67 @@ class Bot: prompt = n.group(1) if self.api_key is not None: try: - asyncio.create_task(self.chat(room_id, - reply_to_event_id, - prompt, - sender_id, - raw_user_message - ) - ) + asyncio.create_task( + self.chat( + room_id, + reply_to_event_id, + prompt, + sender_id, + raw_user_message, + ) + ) except Exception as e: logger.error(e, exc_info=True) - await send_room_message(self.client, room_id, - reply_message=str(e)) + await send_room_message( + self.client, room_id, reply_message=str(e) + ) else: logger.warning("No API_KEY provided") - await send_room_message(self.client, room_id, - reply_message="API_KEY not provided") + await send_room_message( + self.client, room_id, reply_message="API_KEY not provided" + ) m = self.gpt_prog.match(content_body) if m: prompt = m.group(1) try: - asyncio.create_task(self.gpt( - room_id, - reply_to_event_id, - prompt, sender_id, - raw_user_message - ) + asyncio.create_task( + self.gpt( + room_id, + reply_to_event_id, + prompt, + sender_id, + raw_user_message, + ) ) except Exception as e: logger.error(e, exc_info=True) await send_room_message(self.client, room_id, reply_message=str(e)) # bing ai - if self.bing_api_endpoint != '': + if self.bing_api_endpoint != "": b = self.bing_prog.match(content_body) if b: prompt = b.group(1) # raw_content_body used for construct formatted_body try: - asyncio.create_task(self.bing( - room_id, - reply_to_event_id, - prompt, - sender_id, - raw_user_message - ) + asyncio.create_task( + self.bing( + room_id, + reply_to_event_id, + prompt, + sender_id, + raw_user_message, + ) ) except Exception as e: logger.error(e, exc_info=True) - await send_room_message(self.client, room_id, - reply_message=str(e)) + await send_room_message( + self.client, room_id, reply_message=str(e) + ) # Image Generation by Microsoft Bing - if self.bing_auth_cookie != '': + if self.bing_auth_cookie != "": i = self.pic_prog.match(content_body) if i: prompt = i.group(1) @@ -266,8 +320,9 @@ class Bot: asyncio.create_task(self.pic(room_id, prompt)) except Exception as e: logger.error(e, exc_info=True) - await send_room_message(self.client, room_id, - reply_message=str(e)) + await send_room_message( + self.client, room_id, reply_message=str(e) + ) # Google's Bard if self.bard_token is not None: @@ -275,13 +330,14 @@ class Bot: if b: prompt = b.group(1) try: - asyncio.create_task(self.bard( - room_id, - reply_to_event_id, - prompt, - sender_id, - raw_user_message - ) + asyncio.create_task( + self.bard( + room_id, + reply_to_event_id, + prompt, + sender_id, + raw_user_message, + ) ) except Exception as e: logger.error(e, exc_info=True) @@ -293,13 +349,63 @@ class Bot: if m: prompt = m.group(1) try: - asyncio.create_task(self.lc( - room_id, - reply_to_event_id, - prompt, - sender_id, - raw_user_message + asyncio.create_task( + self.lc( + room_id, + reply_to_event_id, + prompt, + sender_id, + raw_user_message, + ) ) + except Exception as e: + logger.error(e, exc_info=True) + await send_room_message(self.client, room_id, reply_message={e}) + + # pandora + if self.pandora_api_endpoint is not None: + t = self.talk_prog.match(content_body) + if t: + prompt = t.group(1) + try: + asyncio.create_task( + self.talk( + room_id, + reply_to_event_id, + prompt, + sender_id, + raw_user_message, + ) + ) + except Exception as e: + logger.error(e, exc_info=True) + await send_room_message(self.client, room_id, reply_message={e}) + + g = self.goon_prog.match(content_body) + if g: + try: + asyncio.create_task( + self.goon( + room_id, + reply_to_event_id, + sender_id, + raw_user_message, + ) + ) + except Exception as e: + logger.error(e, exc_info=True) + await send_room_message(self.client, room_id, reply_message={e}) + + n = self.new_prog.match(content_body) + if n: + try: + asyncio.create_task( + self.new( + room_id, + reply_to_event_id, + sender_id, + raw_user_message, + ) ) except Exception as e: logger.error(e, exc_info=True) @@ -317,8 +423,8 @@ class Bot: logger.error( f"Failed to decrypt message: {event.event_id} \ - from {event.sender} in {room.room_id}\n" + - "Please make sure the bot current session is verified" + from {event.sender} in {room.room_id}\n" + + "Please make sure the bot current session is verified" ) # invite_callback event @@ -334,7 +440,8 @@ class Bot: if type(result) == JoinError: logger.error( f"Error joining room {room.room_id} (attempt %d): %s", - attempt, result.message, + attempt, + result.message, ) else: break @@ -356,11 +463,11 @@ class Bot: try: client = self.client logger.debug( - f"Device Event of type {type(event)} received in " - "to_device_cb().") + f"Device Event of type {type(event)} received in " "to_device_cb()." + ) if isinstance(event, KeyVerificationStart): # first step - """ first step: receive KeyVerificationStart + """first step: receive KeyVerificationStart KeyVerificationStart( source={'content': {'method': 'm.sas.v1', @@ -390,12 +497,13 @@ class Bot: """ if "emoji" not in event.short_authentication_string: - estr = ("Other device does not support emoji verification " - f"{event.short_authentication_string}. Aborting.") + estr = ( + "Other device does not support emoji verification " + f"{event.short_authentication_string}. Aborting." + ) logger.info(estr) return - resp = await client.accept_key_verification( - event.transaction_id) + resp = await client.accept_key_verification(event.transaction_id) if isinstance(resp, ToDeviceError): estr = f"accept_key_verification() failed with {resp}" logger.info(estr) @@ -409,7 +517,7 @@ class Bot: logger.info(estr) elif isinstance(event, KeyVerificationCancel): # anytime - """ at any time: receive KeyVerificationCancel + """at any time: receive KeyVerificationCancel KeyVerificationCancel(source={ 'content': {'code': 'm.mismatched_sas', 'reason': 'Mismatched authentication string', @@ -426,12 +534,14 @@ class Bot: # client.cancel_key_verification(tx_id, reject=False) # here. The SAS flow is already cancelled. # We only need to inform the user. - estr = (f"Verification has been cancelled by {event.sender} " - f"for reason \"{event.reason}\".") + estr = ( + f"Verification has been cancelled by {event.sender} " + f'for reason "{event.reason}".' + ) logger.info(estr) elif isinstance(event, KeyVerificationKey): # second step - """ Second step is to receive KeyVerificationKey + """Second step is to receive KeyVerificationKey KeyVerificationKey( source={'content': { 'key': 'SomeCryptoKey', @@ -456,36 +566,38 @@ class Bot: # automatic match, so we use y yn = "y" if yn.lower() == "y": - estr = ("Match! The verification for this " - "device will be accepted.") + estr = ( + "Match! The verification for this " "device will be accepted." + ) logger.info(estr) - resp = await client.confirm_short_auth_string( - event.transaction_id) + resp = await client.confirm_short_auth_string(event.transaction_id) if isinstance(resp, ToDeviceError): - estr = ("confirm_short_auth_string() " - f"failed with {resp}") + estr = "confirm_short_auth_string() " f"failed with {resp}" logger.info(estr) elif yn.lower() == "n": # no, don't match, reject - estr = ("No match! Device will NOT be verified " - "by rejecting verification.") + estr = ( + "No match! Device will NOT be verified " + "by rejecting verification." + ) logger.info(estr) resp = await client.cancel_key_verification( - event.transaction_id, reject=True) + event.transaction_id, reject=True + ) if isinstance(resp, ToDeviceError): - estr = (f"cancel_key_verification failed with {resp}") + estr = f"cancel_key_verification failed with {resp}" logger.info(estr) else: # C or anything for cancel - estr = ("Cancelled by user! Verification will be " - "cancelled.") + estr = "Cancelled by user! Verification will be " "cancelled." logger.info(estr) resp = await client.cancel_key_verification( - event.transaction_id, reject=False) + event.transaction_id, reject=False + ) if isinstance(resp, ToDeviceError): - estr = (f"cancel_key_verification failed with {resp}") + estr = f"cancel_key_verification failed with {resp}" logger.info(estr) elif isinstance(event, KeyVerificationMac): # third step - """ Third step is to receive KeyVerificationMac + """Third step is to receive KeyVerificationMac KeyVerificationMac( source={'content': { 'mac': {'ed25519:DEVICEIDXY': 'SomeKey1', @@ -505,185 +617,265 @@ class Bot: todevice_msg = sas.get_mac() except LocalProtocolError as e: # e.g. it might have been cancelled by ourselves - estr = (f"Cancelled or protocol error: Reason: {e}.\n" - f"Verification with {event.sender} not concluded. " - "Try again?") + estr = ( + f"Cancelled or protocol error: Reason: {e}.\n" + f"Verification with {event.sender} not concluded. " + "Try again?" + ) logger.info(estr) else: resp = await client.to_device(todevice_msg) if isinstance(resp, ToDeviceError): estr = f"to_device failed with {resp}" logger.info(estr) - estr = (f"sas.we_started_it = {sas.we_started_it}\n" - f"sas.sas_accepted = {sas.sas_accepted}\n" - f"sas.canceled = {sas.canceled}\n" - f"sas.timed_out = {sas.timed_out}\n" - f"sas.verified = {sas.verified}\n" - f"sas.verified_devices = {sas.verified_devices}\n") + estr = ( + f"sas.we_started_it = {sas.we_started_it}\n" + f"sas.sas_accepted = {sas.sas_accepted}\n" + f"sas.canceled = {sas.canceled}\n" + f"sas.timed_out = {sas.timed_out}\n" + f"sas.verified = {sas.verified}\n" + f"sas.verified_devices = {sas.verified_devices}\n" + ) logger.info(estr) - estr = ("Emoji verification was successful!\n" - "Initiate another Emoji verification from " - "another device or room if desired. " - "Or if done verifying, hit Control-C to stop the " - "bot in order to restart it as a service or to " - "run it in the background.") + estr = ( + "Emoji verification was successful!\n" + "Initiate another Emoji verification from " + "another device or room if desired. " + "Or if done verifying, hit Control-C to stop the " + "bot in order to restart it as a service or to " + "run it in the background." + ) logger.info(estr) else: - estr = (f"Received unexpected event type {type(event)}. " - f"Event is {event}. Event will be ignored.") + estr = ( + f"Received unexpected event type {type(event)}. " + f"Event is {event}. Event will be ignored." + ) logger.info(estr) except BaseException: estr = traceback.format_exc() logger.info(estr) # !chat command - async def chat(self, room_id, reply_to_event_id, prompt, sender_id, - raw_user_message): + async def chat( + self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message + ): await self.client.room_typing(room_id, timeout=120000) - try: - text = await self.chatbot.ask_async(prompt) - except Exception as e: - raise Exception(e) - try: - text = text.strip() - await send_room_message(self.client, room_id, reply_message=text, - reply_to_event_id="", sender_id=sender_id, - user_message=raw_user_message, - markdown_formatted=self.markdown_formatted) - except Exception as e: - logger.error(f"Error: {e}", exc_info=True) + text = await self.chatbot.ask_async(prompt) + text = text.strip() + await send_room_message( + self.client, + room_id, + reply_message=text, + reply_to_event_id="", + sender_id=sender_id, + user_message=raw_user_message, + markdown_formatted=self.markdown_formatted, + ) # !gpt command - async def gpt(self, room_id, reply_to_event_id, prompt, sender_id, - raw_user_message) -> None: - try: - # sending typing state - await self.client.room_typing(room_id, timeout=240000) - # timeout 240s - text = await asyncio.wait_for(self.askgpt.oneTimeAsk(prompt, - self.chatgpt_api_endpoint, - self.headers), - timeout=240) - except TimeoutError: - logger.error("TimeoutException", exc_info=True) - raise Exception("Timeout error") - except Exception as e: - raise Exception(e) + async def gpt( + self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message + ) -> None: + # sending typing state + await self.client.room_typing(room_id, timeout=240000) + # timeout 240s + text = await asyncio.wait_for( + self.askgpt.oneTimeAsk(prompt, self.chatgpt_api_endpoint, self.headers), + timeout=240, + ) - try: - text = text.strip() - await send_room_message(self.client, room_id, reply_message=text, - reply_to_event_id="", sender_id=sender_id, - user_message=raw_user_message, - markdown_formatted=self.markdown_formatted) - except Exception as e: - logger.error(f"Error: {e}", exc_info=True) + text = text.strip() + await send_room_message( + self.client, + room_id, + reply_message=text, + reply_to_event_id="", + sender_id=sender_id, + user_message=raw_user_message, + markdown_formatted=self.markdown_formatted, + ) # !bing command - async def bing(self, room_id, reply_to_event_id, prompt, sender_id, - raw_user_message) -> None: - try: - # sending typing state - await self.client.room_typing(room_id, timeout=180000) - # timeout 240s - text = await asyncio.wait_for(self.bingbot.ask_bing(prompt), timeout=240) - except TimeoutError: - logger.error("timeoutException", exc_info=True) - raise Exception("Timeout error") - except Exception as e: - raise Exception(e) + async def bing( + self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message + ) -> None: + # sending typing state + await self.client.room_typing(room_id, timeout=180000) + # timeout 240s + text = await asyncio.wait_for(self.bingbot.ask_bing(prompt), timeout=240) - try: - text = text.strip() - await send_room_message(self.client, room_id, reply_message=text, - reply_to_event_id="", sender_id=sender_id, - user_message=raw_user_message, - markdown_formatted=self.markdown_formatted) - except Exception as e: - logger.error(e, exc_info=True) + text = text.strip() + await send_room_message( + self.client, + room_id, + reply_message=text, + reply_to_event_id="", + sender_id=sender_id, + user_message=raw_user_message, + markdown_formatted=self.markdown_formatted, + ) # !bard command - async def bard(self, room_id, reply_to_event_id, prompt, sender_id, - raw_user_message) -> None: - try: - # sending typing state - await self.client.room_typing(room_id) - response = await asyncio.to_thread(self.bardbot.ask, prompt) - except Exception as e: - raise Exception(e) + async def bard( + self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message + ) -> None: + # sending typing state + await self.client.room_typing(room_id) + response = await asyncio.to_thread(self.bardbot.ask, prompt) - try: - content = str(response['content']).strip() - await send_room_message(self.client, room_id, reply_message=content, - reply_to_event_id="", sender_id=sender_id, - user_message=raw_user_message, - markdown_formatted=self.markdown_formatted) - except Exception as e: - logger.error(e, exc_info=True) + content = str(response["content"]).strip() + await send_room_message( + self.client, + room_id, + reply_message=content, + reply_to_event_id="", + sender_id=sender_id, + user_message=raw_user_message, + markdown_formatted=self.markdown_formatted, + ) # !lc command - async def lc(self, room_id, reply_to_event_id, prompt, sender_id, - raw_user_message) -> None: - try: - # sending typing state - await self.client.room_typing(room_id) - if self.flowise_api_key is not None: - headers = {'Authorization': f'Bearer {self.flowise_api_key}'} - response = await asyncio.to_thread(flowise_query, - self.flowise_api_url, prompt, headers) - else: - response = await asyncio.to_thread(flowise_query, - self.flowise_api_url, prompt) - await send_room_message(self.client, room_id, reply_message=response, - reply_to_event_id="", sender_id=sender_id, - user_message=raw_user_message, - markdown_formatted=self.markdown_formatted) - except Exception as e: - raise Exception(e) + async def lc( + self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message + ) -> None: + # sending typing state + await self.client.room_typing(room_id) + if self.flowise_api_key is not None: + headers = {"Authorization": f"Bearer {self.flowise_api_key}"} + response = await asyncio.to_thread( + flowise_query, self.flowise_api_url, prompt, headers + ) + else: + response = await asyncio.to_thread( + flowise_query, self.flowise_api_url, prompt + ) + await send_room_message( + self.client, + room_id, + reply_message=response, + reply_to_event_id="", + sender_id=sender_id, + user_message=raw_user_message, + markdown_formatted=self.markdown_formatted, + ) + + # !talk command + async def talk( + self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message + ) -> None: + if self.pandora_data[sender_id]["conversation_id"] is not None: + data = { + "prompt": prompt, + "model": self.pandora_api_model, + "parent_message_id": self.pandora_data[sender_id]["parent_message_id"], + "conversation_id": self.pandora_data[sender_id]["conversation_id"], + "stream": False, + } + else: + data = { + "prompt": prompt, + "model": self.pandora_api_model, + "parent_message_id": self.pandora_data[sender_id]["parent_message_id"], + "stream": False, + } + # sending typing state + await self.client.room_typing(room_id) + response = await self.pandora.talk(data) + self.pandora_data[sender_id]["conversation_id"] = response["conversation_id"] + self.pandora_data[sender_id]["parent_message_id"] = response["message"]["id"] + content = response["message"]["content"]["parts"][0] + if self.pandora_data[sender_id]["first_time"]: + self.pandora_data[sender_id]["first_time"] = False + data = { + "model": self.pandora_api_model, + "message_id": self.pandora_data[sender_id]["parent_message_id"], + } + await self.pandora.gen_title( + data, self.pandora_data[sender_id]["conversation_id"] + ) + await send_room_message( + self.client, + room_id, + reply_message=content, + reply_to_event_id="", + sender_id=sender_id, + user_message=raw_user_message, + markdown_formatted=self.markdown_formatted, + ) + + # !goon command + async def goon( + self, room_id, reply_to_event_id, sender_id, raw_user_message + ) -> None: + # sending typing state + await self.client.room_typing(room_id) + data = { + "model": self.pandora_api_model, + "parent_message_id": self.pandora_data[sender_id]["parent_message_id"], + "conversation_id": self.pandora_data[sender_id]["conversation_id"], + "stream": False, + } + response = await self.pandora.goon(data) + self.pandora_data[sender_id]["conversation_id"] = response["conversation_id"] + self.pandora_data[sender_id]["parent_message_id"] = response["message"]["id"] + content = response["message"]["content"]["parts"][0] + await send_room_message( + self.client, + room_id, + reply_message=content, + reply_to_event_id="", + sender_id=sender_id, + user_message=raw_user_message, + markdown_formatted=self.markdown_formatted, + ) + + # !new command + async def new( + self, room_id, reply_to_event_id, sender_id, raw_user_message + ) -> None: + self.pandora_init(sender_id) + content = "New conversation created, please use !talk to start chatting!" + await send_room_message( + self.client, + room_id, + reply_message=content, + reply_to_event_id="", + sender_id=sender_id, + user_message=raw_user_message, + markdown_formatted=self.markdown_formatted, + ) # !pic command - async def pic(self, room_id, prompt): - try: - await self.client.room_typing(room_id, timeout=180000) - # generate image - try: - - links = await self.imageGen.get_images(prompt) - image_path_list = await self.imageGen.save_images(links, "images", - self.output_four_images) - except Exception as e: - logger.error(f"Image Generation error: {e}", exc_info=True) - raise Exception(e) - - # send image - try: - for image_path in image_path_list: - await send_room_image(self.client, room_id, image_path) - await self.client.room_typing(room_id, typing_state=False) - except Exception as e: - logger.error(e, exc_info=True) - - except Exception as e: - logger.error(e, exc_info=True) + await self.client.room_typing(room_id, timeout=180000) + # generate image + links = await self.imageGen.get_images(prompt) + image_path_list = await self.imageGen.save_images( + links, "images", self.output_four_images + ) + # send image + for image_path in image_path_list: + await send_room_image(self.client, room_id, image_path) + await self.client.room_typing(room_id, typing_state=False) # !help command async def help(self, room_id): - try: - # sending typing state - await self.client.room_typing(room_id) - help_info = "!gpt [prompt], generate response without context conversation\n" + \ - "!chat [prompt], chat with context conversation\n" + \ - "!bing [prompt], chat with context conversation powered by Bing AI\n" + \ - "!bard [prompt], chat with Google's Bard\n" + \ - "!pic [prompt], Image generation by Microsoft Bing\n" + \ - "!lc [prompt], chat using langchain api\n" + \ - "!help, help message" # noqa: E501 + help_info = ( + "!gpt [prompt], generate response without context conversation\n" + + "!chat [prompt], chat with context conversation\n" + + "!bing [prompt], chat with context conversation powered by Bing AI\n" + + "!bard [prompt], chat with Google's Bard\n" + + "!pic [prompt], Image generation by Microsoft Bing\n" + + "!talk [content], talk using chatgpt web (pandora)\n" + + "!goon, continue the incomplete conversation (pandora)\n" + + "!new, start a new conversation (pandora)\n" + + "!lc [prompt], chat using langchain api\n" + + "!help, help message" + ) # noqa: E501 - await send_room_message(self.client, room_id, reply_message=help_info) - except Exception as e: - logger.error(e, exc_info=True) + await send_room_message(self.client, room_id, reply_message=help_info) # bot login async def login(self) -> None: @@ -702,16 +894,15 @@ class Bot: # import keys async def import_keys(self): resp = await self.client.import_keys( - self.import_keys_path, - self.import_keys_password + self.import_keys_path, self.import_keys_password ) if isinstance(resp, EncryptionError): logger.error(f"import_keys failed with {resp}") else: logger.info( - "import_keys success, please remove import_keys configuration!!!") + "import_keys success, please remove import_keys configuration!!!" + ) # sync messages in the room async def sync_forever(self, timeout=30000, full_state=True) -> None: - await self.client.sync_forever(timeout=timeout, full_state=full_state) diff --git a/compose.yaml b/compose.yaml index b91dabb..ecbe2bc 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ services: app: image: hibobmaster/matrixchatgptbot:latest container_name: matrix_chatgpt_bot - restart: always + restart: unless-stopped # build: # context: . # dockerfile: ./Dockerfile @@ -19,13 +19,23 @@ services: - matrix_network # api: # # bing api - # image: ghcr.io/waylaidwanderer/node-chatgpt-api:latest + # image: hibobmaster/node-chatgpt-api:latest # container_name: node-chatgpt-api - # restart: always + # restart: unless-stopped # volumes: # - ./settings.js:/var/chatgpt-api/settings.js # networks: # - matrix_network + # pandora: + # image: pengzhile/pandora + # container_name: pandora + # restart: unless-stopped + # environment: + # - PANDORA_ACCESS_TOKEN="xxxxxxxxxxxxxx" + # - PANDORA_SERVER="0.0.0.0:8008" + # networks: + # - matrix_network + networks: matrix_network: diff --git a/config.json.sample b/config.json.sample index 2fd86c1..473425d 100644 --- a/config.json.sample +++ b/config.json.sample @@ -15,5 +15,7 @@ "import_keys_path": "element-keys.txt", "import_keys_password": "xxxxxxxxx", "flowise_api_url": "http://localhost:3000/api/v1/prediction/6deb3c89-45bf-4ac4-a0b0-b2d5ef249d21", - "flowise_api_key": "U3pe0bbVDWOyoJtsDzFJjRvHKTP3FRjODwuM78exC3A=" + "flowise_api_key": "U3pe0bbVDWOyoJtsDzFJjRvHKTP3FRjODwuM78exC3A=", + "pandora_api_endpoint": "http://127.0.0.1:8008", + "pandora_api_model": "text-davinci-002-render-sha-mobile" } diff --git a/main.py b/main.py index 7fd89e6..3ab8233 100644 --- a/main.py +++ b/main.py @@ -9,61 +9,66 @@ logger = getlogger() async def main(): need_import_keys = False - if os.path.exists('config.json'): - fp = open('config.json', 'r', encoding="utf8") + if os.path.exists("config.json"): + fp = open("config.json", "r", encoding="utf8") config = json.load(fp) - matrix_bot = Bot(homeserver=config.get('homeserver'), - user_id=config.get('user_id'), - password=config.get('password'), - device_id=config.get('device_id'), - room_id=config.get('room_id'), - api_key=config.get('api_key'), - bing_api_endpoint=config.get('bing_api_endpoint'), - access_token=config.get('access_token'), - bard_token=config.get('bard_token'), - jailbreakEnabled=config.get('jailbreakEnabled'), - bing_auth_cookie=config.get('bing_auth_cookie'), - markdown_formatted=config.get('markdown_formatted'), - output_four_images=config.get('output_four_images'), - import_keys_path=config.get('import_keys_path'), - import_keys_password=config.get( - 'import_keys_password'), - flowise_api_url=config.get('flowise_api_url'), - flowise_api_key=config.get('flowise_api_key'), - ) - if config.get('import_keys_path') and \ - config.get('import_keys_password') is not None: + matrix_bot = Bot( + homeserver=config.get("homeserver"), + user_id=config.get("user_id"), + password=config.get("password"), + device_id=config.get("device_id"), + room_id=config.get("room_id"), + api_key=config.get("api_key"), + bing_api_endpoint=config.get("bing_api_endpoint"), + access_token=config.get("access_token"), + bard_token=config.get("bard_token"), + jailbreakEnabled=config.get("jailbreakEnabled"), + bing_auth_cookie=config.get("bing_auth_cookie"), + markdown_formatted=config.get("markdown_formatted"), + output_four_images=config.get("output_four_images"), + import_keys_path=config.get("import_keys_path"), + import_keys_password=config.get("import_keys_password"), + flowise_api_url=config.get("flowise_api_url"), + flowise_api_key=config.get("flowise_api_key"), + pandora_api_endpoint=config.get("pandora_api_endpoint"), + pandora_api_model=config.get("pandora_api_model"), + ) + if ( + config.get("import_keys_path") + and config.get("import_keys_password") is not None + ): need_import_keys = True else: - matrix_bot = Bot(homeserver=os.environ.get('HOMESERVER'), - user_id=os.environ.get('USER_ID'), - password=os.environ.get('PASSWORD'), - device_id=os.environ.get("DEVICE_ID"), - room_id=os.environ.get("ROOM_ID"), - api_key=os.environ.get("OPENAI_API_KEY"), - bing_api_endpoint=os.environ.get("BING_API_ENDPOINT"), - access_token=os.environ.get("ACCESS_TOKEN"), - bard_token=os.environ.get("BARD_TOKEN"), - jailbreakEnabled=os.environ.get( - "JAILBREAKENABLED", "false").lower() \ - in ('true', '1', 't'), - bing_auth_cookie=os.environ.get("BING_AUTH_COOKIE"), - markdown_formatted=os.environ.get( - "MARKDOWN_FORMATTED", "false").lower() \ - in ('true', '1', 't'), - output_four_images=os.environ.get( - "OUTPUT_FOUR_IMAGES", "false").lower() \ - in ('true', '1', 't'), - import_keys_path=os.environ.get("IMPORT_KEYS_PATH"), - import_keys_password=os.environ.get( - "IMPORT_KEYS_PASSWORD"), - flowise_api_url=os.environ.get("FLOWISE_API_URL"), - flowise_api_key=os.environ.get("FLOWISE_API_KEY"), - ) - if os.environ.get("IMPORT_KEYS_PATH") \ - and os.environ.get("IMPORT_KEYS_PASSWORD") is not None: + matrix_bot = Bot( + homeserver=os.environ.get("HOMESERVER"), + user_id=os.environ.get("USER_ID"), + password=os.environ.get("PASSWORD"), + device_id=os.environ.get("DEVICE_ID"), + room_id=os.environ.get("ROOM_ID"), + api_key=os.environ.get("OPENAI_API_KEY"), + bing_api_endpoint=os.environ.get("BING_API_ENDPOINT"), + access_token=os.environ.get("ACCESS_TOKEN"), + bard_token=os.environ.get("BARD_TOKEN"), + jailbreakEnabled=os.environ.get("JAILBREAKENABLED", "false").lower() + in ("true", "1", "t"), + bing_auth_cookie=os.environ.get("BING_AUTH_COOKIE"), + markdown_formatted=os.environ.get("MARKDOWN_FORMATTED", "false").lower() + in ("true", "1", "t"), + output_four_images=os.environ.get("OUTPUT_FOUR_IMAGES", "false").lower() + in ("true", "1", "t"), + import_keys_path=os.environ.get("IMPORT_KEYS_PATH"), + import_keys_password=os.environ.get("IMPORT_KEYS_PASSWORD"), + flowise_api_url=os.environ.get("FLOWISE_API_URL"), + flowise_api_key=os.environ.get("FLOWISE_API_KEY"), + pandora_api_endpoint=os.environ.get("PANDORA_API_ENDPOINT"), + pandora_api_model=os.environ.get("PANDORA_API_MODEL"), + ) + if ( + os.environ.get("IMPORT_KEYS_PATH") + and os.environ.get("IMPORT_KEYS_PASSWORD") is not None + ): need_import_keys = True await matrix_bot.login() diff --git a/pandora.py b/pandora.py new file mode 100644 index 0000000..c6b5997 --- /dev/null +++ b/pandora.py @@ -0,0 +1,106 @@ +# 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, clientSession: aiohttp.ClientSession) -> None: + self.api_endpoint = api_endpoint.rstrip("/") + self.session = 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" + async with aiohttp.ClientSession() as session: + client = Pandora(api_endpoint, session) + 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())