feat: Support chatting with chatGPT web

This commit is contained in:
hibobmaster 2023-05-30 09:59:25 +08:00
parent 600c5e161a
commit c72e6d6f8e
Signed by: bobmaster
SSH key fingerprint: SHA256:5ZYgd8fg+PcNZNy4SzcSKu5JtqZyBF8kUhY7/k2viDk
8 changed files with 633 additions and 312 deletions

View file

@ -16,3 +16,5 @@ IMPORT_KEYS_PATH="element-keys.txt" # Optional
IMPORT_KEYS_PASSWORD="xxxxxxx" # Optional IMPORT_KEYS_PASSWORD="xxxxxxx" # Optional
FLOWISE_API_URL="http://localhost:3000/api/v1/prediction/xxxx" # Optional FLOWISE_API_URL="http://localhost:3000/api/v1/prediction/xxxx" # Optional
FLOWISE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxx" # Optional FLOWISE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxx" # Optional
PANDORA_API_ENDPOINT="http://pandora:8008" # Optional
PANDORA_API_MODEL="text-davinci-002-render-sha-mobile" # Optional

View file

@ -1,3 +0,0 @@
{
"python.analysis.typeCheckingMode": "off"
}

View file

@ -1,16 +1,18 @@
## Introduction ## 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) ![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) ![image](https://user-images.githubusercontent.com/32976627/232036790-e830145c-914e-40be-b3e6-c02cba93329c.png)
![ChatGPT](https://i.imgur.com/kK4rnPf.jpeg) ![ChatGPT](https://i.imgur.com/kK4rnPf.jpeg)
## Feature ## 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 2. Support Bing Image Creator
3. Support E2E Encrypted Room 3. Support E2E Encrypted Room
4. Colorful code blocks 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 ## 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 !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 ## Bing AI and Image Generation

563
bot.py
View file

@ -4,13 +4,27 @@ import re
import sys import sys
import traceback import traceback
from typing import Union, Optional from typing import Union, Optional
import uuid
import aiohttp import aiohttp
from nio import (AsyncClient, AsyncClientConfig, InviteMemberEvent, JoinError, from nio import (
KeyVerificationCancel, KeyVerificationEvent, EncryptionError, AsyncClient,
KeyVerificationKey, KeyVerificationMac, KeyVerificationStart, AsyncClientConfig,
LocalProtocolError, LoginResponse, MatrixRoom, MegolmEvent, InviteMemberEvent,
RoomMessageText, ToDeviceError) JoinError,
KeyVerificationCancel,
KeyVerificationEvent,
EncryptionError,
KeyVerificationKey,
KeyVerificationMac,
KeyVerificationStart,
LocalProtocolError,
LoginResponse,
MatrixRoom,
MegolmEvent,
RoomMessageText,
ToDeviceError,
)
from nio.store.database import SqliteStore from nio.store.database import SqliteStore
from askgpt import askGPT from askgpt import askGPT
@ -22,6 +36,7 @@ from send_message import send_room_message
from v3 import Chatbot from v3 import Chatbot
from bard import Bardbot from bard import Bardbot
from flowise import flowise_query from flowise import flowise_query
from pandora import Pandora
logger = getlogger() logger = getlogger()
@ -32,8 +47,8 @@ class Bot:
homeserver: str, homeserver: str,
user_id: str, user_id: str,
device_id: str, device_id: str,
chatgpt_api_endpoint: str = os.environ.get( chatgpt_api_endpoint: str = os.environ.get("CHATGPT_API_ENDPOINT")
"CHATGPT_API_ENDPOINT") or "https://api.openai.com/v1/chat/completions", or "https://api.openai.com/v1/chat/completions",
api_key: Union[str, None] = None, api_key: Union[str, None] = None,
room_id: Union[str, None] = None, room_id: Union[str, None] = None,
bing_api_endpoint: Union[str, None] = None, bing_api_endpoint: Union[str, None] = None,
@ -41,20 +56,21 @@ class Bot:
access_token: Union[str, None] = None, access_token: Union[str, None] = None,
bard_token: Union[str, None] = None, bard_token: Union[str, None] = None,
jailbreakEnabled: Union[bool, None] = True, jailbreakEnabled: Union[bool, None] = True,
bing_auth_cookie: Union[str, None] = '', bing_auth_cookie: Union[str, None] = "",
markdown_formatted: Union[bool, None] = False, markdown_formatted: Union[bool, None] = False,
output_four_images: Union[bool, None] = False, output_four_images: Union[bool, None] = False,
import_keys_path: Optional[str] = None, import_keys_path: Optional[str] = None,
import_keys_password: Optional[str] = None, import_keys_password: Optional[str] = None,
flowise_api_url: 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 if homeserver is None or user_id is None or device_id is None:
or device_id is None):
logger.warning("homeserver && user_id && device_id is required") logger.warning("homeserver && user_id && device_id is required")
sys.exit(1) 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") logger.warning("password or access_toekn is required")
sys.exit(1) sys.exit(1)
@ -75,7 +91,7 @@ class Bot:
self.session = aiohttp.ClientSession() self.session = aiohttp.ClientSession()
if bing_api_endpoint is None: if bing_api_endpoint is None:
self.bing_api_endpoint = '' self.bing_api_endpoint = ""
else: else:
self.bing_api_endpoint = bing_api_endpoint self.bing_api_endpoint = bing_api_endpoint
@ -85,7 +101,7 @@ class Bot:
self.jailbreakEnabled = jailbreakEnabled self.jailbreakEnabled = jailbreakEnabled
if bing_auth_cookie is None: if bing_auth_cookie is None:
self.bing_auth_cookie = '' self.bing_auth_cookie = ""
else: else:
self.bing_auth_cookie = bing_auth_cookie self.bing_auth_cookie = bing_auth_cookie
@ -101,27 +117,30 @@ class Bot:
# initialize AsyncClient object # initialize AsyncClient object
self.store_path = os.getcwd() self.store_path = os.getcwd()
self.config = AsyncClientConfig(store=SqliteStore, self.config = AsyncClientConfig(
store=SqliteStore,
store_name="db", store_name="db",
store_sync_tokens=True, store_sync_tokens=True,
encryption_enabled=True, encryption_enabled=True,
) )
self.client = AsyncClient(homeserver=self.homeserver, user=self.user_id, self.client = AsyncClient(
homeserver=self.homeserver,
user=self.user_id,
device_id=self.device_id, device_id=self.device_id,
config=self.config, store_path=self.store_path,) config=self.config,
store_path=self.store_path,
)
if self.access_token is not None: if self.access_token is not None:
self.client.access_token = self.access_token self.client.access_token = self.access_token
# setup event callbacks # setup event callbacks
self.client.add_event_callback( self.client.add_event_callback(self.message_callback, (RoomMessageText,))
self.message_callback, (RoomMessageText, )) self.client.add_event_callback(self.decryption_failure, (MegolmEvent,))
self.client.add_event_callback( self.client.add_event_callback(self.invite_callback, (InviteMemberEvent,))
self.decryption_failure, (MegolmEvent, ))
self.client.add_event_callback(
self.invite_callback, (InviteMemberEvent, ))
self.client.add_to_device_callback( self.client.add_to_device_callback(
self.to_device_callback, (KeyVerificationEvent, )) self.to_device_callback, (KeyVerificationEvent,)
)
# regular expression to match keyword commands # regular expression to match keyword commands
self.gpt_prog = re.compile(r"^\s*!gpt\s*(.+)$") 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.pic_prog = re.compile(r"^\s*!pic\s*(.+)$")
self.lc_prog = re.compile(r"^\s*!lc\s*(.+)$") self.lc_prog = re.compile(r"^\s*!lc\s*(.+)$")
self.help_prog = re.compile(r"^\s*!help\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 # initialize chatbot and chatgpt_api_endpoint
if self.api_key is not None: if self.api_key is not None:
@ -147,18 +169,32 @@ class Bot:
self.askgpt = askGPT(self.session) self.askgpt = askGPT(self.session)
# initialize bingbot # initialize bingbot
if self.bing_api_endpoint != '': if self.bing_api_endpoint != "":
self.bingbot = BingBot( self.bingbot = BingBot(
self.session, bing_api_endpoint, jailbreakEnabled=self.jailbreakEnabled) self.session, bing_api_endpoint, jailbreakEnabled=self.jailbreakEnabled
)
# initialize BingImageGenAsync # initialize BingImageGenAsync
if self.bing_auth_cookie != '': if self.bing_auth_cookie != "":
self.imageGen = ImageGenAsync(self.bing_auth_cookie, quiet=True) self.imageGen = ImageGenAsync(self.bing_auth_cookie, quiet=True)
# initialize Bardbot # initialize Bardbot
if bard_token is not None: if bard_token is not None:
self.bardbot = Bardbot(self.bard_token) 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): def __del__(self):
try: try:
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
@ -170,6 +206,13 @@ class Bot:
async def _close(self): async def _close(self):
await self.session.close() 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 # message_callback RoomMessageText event
async def message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None: async def message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None:
if self.room_id is None: if self.room_id is None:
@ -186,6 +229,9 @@ class Bot:
# sender_id # sender_id
sender_id = event.sender sender_id = event.sender
if sender_id not in self.pandora_data:
self.pandora_init(sender_id)
# user_message # user_message
raw_user_message = event.body raw_user_message = event.body
@ -206,31 +252,37 @@ class Bot:
prompt = n.group(1) prompt = n.group(1)
if self.api_key is not None: if self.api_key is not None:
try: try:
asyncio.create_task(self.chat(room_id, asyncio.create_task(
self.chat(
room_id,
reply_to_event_id, reply_to_event_id,
prompt, prompt,
sender_id, sender_id,
raw_user_message raw_user_message,
) )
) )
except Exception as e: except Exception as e:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
await send_room_message(self.client, room_id, await send_room_message(
reply_message=str(e)) self.client, room_id, reply_message=str(e)
)
else: else:
logger.warning("No API_KEY provided") logger.warning("No API_KEY provided")
await send_room_message(self.client, room_id, await send_room_message(
reply_message="API_KEY not provided") self.client, room_id, reply_message="API_KEY not provided"
)
m = self.gpt_prog.match(content_body) m = self.gpt_prog.match(content_body)
if m: if m:
prompt = m.group(1) prompt = m.group(1)
try: try:
asyncio.create_task(self.gpt( asyncio.create_task(
self.gpt(
room_id, room_id,
reply_to_event_id, reply_to_event_id,
prompt, sender_id, prompt,
raw_user_message sender_id,
raw_user_message,
) )
) )
except Exception as e: except Exception as e:
@ -238,27 +290,29 @@ class Bot:
await send_room_message(self.client, room_id, reply_message=str(e)) await send_room_message(self.client, room_id, reply_message=str(e))
# bing ai # bing ai
if self.bing_api_endpoint != '': if self.bing_api_endpoint != "":
b = self.bing_prog.match(content_body) b = self.bing_prog.match(content_body)
if b: if b:
prompt = b.group(1) prompt = b.group(1)
# raw_content_body used for construct formatted_body # raw_content_body used for construct formatted_body
try: try:
asyncio.create_task(self.bing( asyncio.create_task(
self.bing(
room_id, room_id,
reply_to_event_id, reply_to_event_id,
prompt, prompt,
sender_id, sender_id,
raw_user_message raw_user_message,
) )
) )
except Exception as e: except Exception as e:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
await send_room_message(self.client, room_id, await send_room_message(
reply_message=str(e)) self.client, room_id, reply_message=str(e)
)
# Image Generation by Microsoft Bing # Image Generation by Microsoft Bing
if self.bing_auth_cookie != '': if self.bing_auth_cookie != "":
i = self.pic_prog.match(content_body) i = self.pic_prog.match(content_body)
if i: if i:
prompt = i.group(1) prompt = i.group(1)
@ -266,8 +320,9 @@ class Bot:
asyncio.create_task(self.pic(room_id, prompt)) asyncio.create_task(self.pic(room_id, prompt))
except Exception as e: except Exception as e:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
await send_room_message(self.client, room_id, await send_room_message(
reply_message=str(e)) self.client, room_id, reply_message=str(e)
)
# Google's Bard # Google's Bard
if self.bard_token is not None: if self.bard_token is not None:
@ -275,12 +330,13 @@ class Bot:
if b: if b:
prompt = b.group(1) prompt = b.group(1)
try: try:
asyncio.create_task(self.bard( asyncio.create_task(
self.bard(
room_id, room_id,
reply_to_event_id, reply_to_event_id,
prompt, prompt,
sender_id, sender_id,
raw_user_message raw_user_message,
) )
) )
except Exception as e: except Exception as e:
@ -293,12 +349,62 @@ class Bot:
if m: if m:
prompt = m.group(1) prompt = m.group(1)
try: try:
asyncio.create_task(self.lc( asyncio.create_task(
self.lc(
room_id, room_id,
reply_to_event_id, reply_to_event_id,
prompt, prompt,
sender_id, sender_id,
raw_user_message 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: except Exception as e:
@ -317,8 +423,8 @@ class Bot:
logger.error( logger.error(
f"Failed to decrypt message: {event.event_id} \ f"Failed to decrypt message: {event.event_id} \
from {event.sender} in {room.room_id}\n" + from {event.sender} in {room.room_id}\n"
"Please make sure the bot current session is verified" + "Please make sure the bot current session is verified"
) )
# invite_callback event # invite_callback event
@ -334,7 +440,8 @@ class Bot:
if type(result) == JoinError: if type(result) == JoinError:
logger.error( logger.error(
f"Error joining room {room.room_id} (attempt %d): %s", f"Error joining room {room.room_id} (attempt %d): %s",
attempt, result.message, attempt,
result.message,
) )
else: else:
break break
@ -356,11 +463,11 @@ class Bot:
try: try:
client = self.client client = self.client
logger.debug( logger.debug(
f"Device Event of type {type(event)} received in " f"Device Event of type {type(event)} received in " "to_device_cb()."
"to_device_cb().") )
if isinstance(event, KeyVerificationStart): # first step if isinstance(event, KeyVerificationStart): # first step
""" first step: receive KeyVerificationStart """first step: receive KeyVerificationStart
KeyVerificationStart( KeyVerificationStart(
source={'content': source={'content':
{'method': 'm.sas.v1', {'method': 'm.sas.v1',
@ -390,12 +497,13 @@ class Bot:
""" """
if "emoji" not in event.short_authentication_string: if "emoji" not in event.short_authentication_string:
estr = ("Other device does not support emoji verification " estr = (
f"{event.short_authentication_string}. Aborting.") "Other device does not support emoji verification "
f"{event.short_authentication_string}. Aborting."
)
logger.info(estr) logger.info(estr)
return return
resp = await client.accept_key_verification( resp = await client.accept_key_verification(event.transaction_id)
event.transaction_id)
if isinstance(resp, ToDeviceError): if isinstance(resp, ToDeviceError):
estr = f"accept_key_verification() failed with {resp}" estr = f"accept_key_verification() failed with {resp}"
logger.info(estr) logger.info(estr)
@ -409,7 +517,7 @@ class Bot:
logger.info(estr) logger.info(estr)
elif isinstance(event, KeyVerificationCancel): # anytime elif isinstance(event, KeyVerificationCancel): # anytime
""" at any time: receive KeyVerificationCancel """at any time: receive KeyVerificationCancel
KeyVerificationCancel(source={ KeyVerificationCancel(source={
'content': {'code': 'm.mismatched_sas', 'content': {'code': 'm.mismatched_sas',
'reason': 'Mismatched authentication string', 'reason': 'Mismatched authentication string',
@ -426,12 +534,14 @@ class Bot:
# client.cancel_key_verification(tx_id, reject=False) # client.cancel_key_verification(tx_id, reject=False)
# here. The SAS flow is already cancelled. # here. The SAS flow is already cancelled.
# We only need to inform the user. # We only need to inform the user.
estr = (f"Verification has been cancelled by {event.sender} " estr = (
f"for reason \"{event.reason}\".") f"Verification has been cancelled by {event.sender} "
f'for reason "{event.reason}".'
)
logger.info(estr) logger.info(estr)
elif isinstance(event, KeyVerificationKey): # second step elif isinstance(event, KeyVerificationKey): # second step
""" Second step is to receive KeyVerificationKey """Second step is to receive KeyVerificationKey
KeyVerificationKey( KeyVerificationKey(
source={'content': { source={'content': {
'key': 'SomeCryptoKey', 'key': 'SomeCryptoKey',
@ -456,36 +566,38 @@ class Bot:
# automatic match, so we use y # automatic match, so we use y
yn = "y" yn = "y"
if yn.lower() == "y": if yn.lower() == "y":
estr = ("Match! The verification for this " estr = (
"device will be accepted.") "Match! The verification for this " "device will be accepted."
)
logger.info(estr) logger.info(estr)
resp = await client.confirm_short_auth_string( resp = await client.confirm_short_auth_string(event.transaction_id)
event.transaction_id)
if isinstance(resp, ToDeviceError): if isinstance(resp, ToDeviceError):
estr = ("confirm_short_auth_string() " estr = "confirm_short_auth_string() " f"failed with {resp}"
f"failed with {resp}")
logger.info(estr) logger.info(estr)
elif yn.lower() == "n": # no, don't match, reject elif yn.lower() == "n": # no, don't match, reject
estr = ("No match! Device will NOT be verified " estr = (
"by rejecting verification.") "No match! Device will NOT be verified "
"by rejecting verification."
)
logger.info(estr) logger.info(estr)
resp = await client.cancel_key_verification( resp = await client.cancel_key_verification(
event.transaction_id, reject=True) event.transaction_id, reject=True
)
if isinstance(resp, ToDeviceError): if isinstance(resp, ToDeviceError):
estr = (f"cancel_key_verification failed with {resp}") estr = f"cancel_key_verification failed with {resp}"
logger.info(estr) logger.info(estr)
else: # C or anything for cancel else: # C or anything for cancel
estr = ("Cancelled by user! Verification will be " estr = "Cancelled by user! Verification will be " "cancelled."
"cancelled.")
logger.info(estr) logger.info(estr)
resp = await client.cancel_key_verification( resp = await client.cancel_key_verification(
event.transaction_id, reject=False) event.transaction_id, reject=False
)
if isinstance(resp, ToDeviceError): if isinstance(resp, ToDeviceError):
estr = (f"cancel_key_verification failed with {resp}") estr = f"cancel_key_verification failed with {resp}"
logger.info(estr) logger.info(estr)
elif isinstance(event, KeyVerificationMac): # third step elif isinstance(event, KeyVerificationMac): # third step
""" Third step is to receive KeyVerificationMac """Third step is to receive KeyVerificationMac
KeyVerificationMac( KeyVerificationMac(
source={'content': { source={'content': {
'mac': {'ed25519:DEVICEIDXY': 'SomeKey1', 'mac': {'ed25519:DEVICEIDXY': 'SomeKey1',
@ -505,185 +617,265 @@ class Bot:
todevice_msg = sas.get_mac() todevice_msg = sas.get_mac()
except LocalProtocolError as e: except LocalProtocolError as e:
# e.g. it might have been cancelled by ourselves # e.g. it might have been cancelled by ourselves
estr = (f"Cancelled or protocol error: Reason: {e}.\n" estr = (
f"Cancelled or protocol error: Reason: {e}.\n"
f"Verification with {event.sender} not concluded. " f"Verification with {event.sender} not concluded. "
"Try again?") "Try again?"
)
logger.info(estr) logger.info(estr)
else: else:
resp = await client.to_device(todevice_msg) resp = await client.to_device(todevice_msg)
if isinstance(resp, ToDeviceError): if isinstance(resp, ToDeviceError):
estr = f"to_device failed with {resp}" estr = f"to_device failed with {resp}"
logger.info(estr) logger.info(estr)
estr = (f"sas.we_started_it = {sas.we_started_it}\n" estr = (
f"sas.we_started_it = {sas.we_started_it}\n"
f"sas.sas_accepted = {sas.sas_accepted}\n" f"sas.sas_accepted = {sas.sas_accepted}\n"
f"sas.canceled = {sas.canceled}\n" f"sas.canceled = {sas.canceled}\n"
f"sas.timed_out = {sas.timed_out}\n" f"sas.timed_out = {sas.timed_out}\n"
f"sas.verified = {sas.verified}\n" f"sas.verified = {sas.verified}\n"
f"sas.verified_devices = {sas.verified_devices}\n") f"sas.verified_devices = {sas.verified_devices}\n"
)
logger.info(estr) logger.info(estr)
estr = ("Emoji verification was successful!\n" estr = (
"Emoji verification was successful!\n"
"Initiate another Emoji verification from " "Initiate another Emoji verification from "
"another device or room if desired. " "another device or room if desired. "
"Or if done verifying, hit Control-C to stop the " "Or if done verifying, hit Control-C to stop the "
"bot in order to restart it as a service or to " "bot in order to restart it as a service or to "
"run it in the background.") "run it in the background."
)
logger.info(estr) logger.info(estr)
else: else:
estr = (f"Received unexpected event type {type(event)}. " estr = (
f"Event is {event}. Event will be ignored.") f"Received unexpected event type {type(event)}. "
f"Event is {event}. Event will be ignored."
)
logger.info(estr) logger.info(estr)
except BaseException: except BaseException:
estr = traceback.format_exc() estr = traceback.format_exc()
logger.info(estr) logger.info(estr)
# !chat command # !chat command
async def chat(self, room_id, reply_to_event_id, prompt, sender_id, async def chat(
raw_user_message): self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message
):
await self.client.room_typing(room_id, timeout=120000) 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 = await self.chatbot.ask_async(prompt)
text = text.strip() text = text.strip()
await send_room_message(self.client, room_id, reply_message=text, await send_room_message(
reply_to_event_id="", sender_id=sender_id, self.client,
room_id,
reply_message=text,
reply_to_event_id="",
sender_id=sender_id,
user_message=raw_user_message, user_message=raw_user_message,
markdown_formatted=self.markdown_formatted) markdown_formatted=self.markdown_formatted,
except Exception as e: )
logger.error(f"Error: {e}", exc_info=True)
# !gpt command # !gpt command
async def gpt(self, room_id, reply_to_event_id, prompt, sender_id, async def gpt(
raw_user_message) -> None: self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message
try: ) -> None:
# sending typing state # sending typing state
await self.client.room_typing(room_id, timeout=240000) await self.client.room_typing(room_id, timeout=240000)
# timeout 240s # timeout 240s
text = await asyncio.wait_for(self.askgpt.oneTimeAsk(prompt, text = await asyncio.wait_for(
self.chatgpt_api_endpoint, self.askgpt.oneTimeAsk(prompt, self.chatgpt_api_endpoint, self.headers),
self.headers), timeout=240,
timeout=240) )
except TimeoutError:
logger.error("TimeoutException", exc_info=True)
raise Exception("Timeout error")
except Exception as e:
raise Exception(e)
try:
text = text.strip() text = text.strip()
await send_room_message(self.client, room_id, reply_message=text, await send_room_message(
reply_to_event_id="", sender_id=sender_id, self.client,
room_id,
reply_message=text,
reply_to_event_id="",
sender_id=sender_id,
user_message=raw_user_message, user_message=raw_user_message,
markdown_formatted=self.markdown_formatted) markdown_formatted=self.markdown_formatted,
except Exception as e: )
logger.error(f"Error: {e}", exc_info=True)
# !bing command # !bing command
async def bing(self, room_id, reply_to_event_id, prompt, sender_id, async def bing(
raw_user_message) -> None: self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message
try: ) -> None:
# sending typing state # sending typing state
await self.client.room_typing(room_id, timeout=180000) await self.client.room_typing(room_id, timeout=180000)
# timeout 240s # timeout 240s
text = await asyncio.wait_for(self.bingbot.ask_bing(prompt), timeout=240) 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)
try:
text = text.strip() text = text.strip()
await send_room_message(self.client, room_id, reply_message=text, await send_room_message(
reply_to_event_id="", sender_id=sender_id, self.client,
room_id,
reply_message=text,
reply_to_event_id="",
sender_id=sender_id,
user_message=raw_user_message, user_message=raw_user_message,
markdown_formatted=self.markdown_formatted) markdown_formatted=self.markdown_formatted,
except Exception as e: )
logger.error(e, exc_info=True)
# !bard command # !bard command
async def bard(self, room_id, reply_to_event_id, prompt, sender_id, async def bard(
raw_user_message) -> None: self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message
try: ) -> None:
# sending typing state # sending typing state
await self.client.room_typing(room_id) await self.client.room_typing(room_id)
response = await asyncio.to_thread(self.bardbot.ask, prompt) response = await asyncio.to_thread(self.bardbot.ask, prompt)
except Exception as e:
raise Exception(e)
try: content = str(response["content"]).strip()
content = str(response['content']).strip() await send_room_message(
await send_room_message(self.client, room_id, reply_message=content, self.client,
reply_to_event_id="", sender_id=sender_id, room_id,
reply_message=content,
reply_to_event_id="",
sender_id=sender_id,
user_message=raw_user_message, user_message=raw_user_message,
markdown_formatted=self.markdown_formatted) markdown_formatted=self.markdown_formatted,
except Exception as e: )
logger.error(e, exc_info=True)
# !lc command # !lc command
async def lc(self, room_id, reply_to_event_id, prompt, sender_id, async def lc(
raw_user_message) -> None: self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message
try: ) -> None:
# sending typing state # sending typing state
await self.client.room_typing(room_id) await self.client.room_typing(room_id)
if self.flowise_api_key is not None: if self.flowise_api_key is not None:
headers = {'Authorization': f'Bearer {self.flowise_api_key}'} headers = {"Authorization": f"Bearer {self.flowise_api_key}"}
response = await asyncio.to_thread(flowise_query, response = await asyncio.to_thread(
self.flowise_api_url, prompt, headers) flowise_query, self.flowise_api_url, prompt, headers
)
else: else:
response = await asyncio.to_thread(flowise_query, response = await asyncio.to_thread(
self.flowise_api_url, prompt) 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, await send_room_message(
self.client,
room_id,
reply_message=response,
reply_to_event_id="",
sender_id=sender_id,
user_message=raw_user_message, user_message=raw_user_message,
markdown_formatted=self.markdown_formatted) markdown_formatted=self.markdown_formatted,
except Exception as e: )
raise Exception(e)
# !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 # !pic command
async def pic(self, room_id, prompt): async def pic(self, room_id, prompt):
try:
await self.client.room_typing(room_id, timeout=180000) await self.client.room_typing(room_id, timeout=180000)
# generate image # generate image
try:
links = await self.imageGen.get_images(prompt) links = await self.imageGen.get_images(prompt)
image_path_list = await self.imageGen.save_images(links, "images", image_path_list = await self.imageGen.save_images(
self.output_four_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 # send image
try:
for image_path in image_path_list: for image_path in image_path_list:
await send_room_image(self.client, room_id, image_path) await send_room_image(self.client, room_id, image_path)
await self.client.room_typing(room_id, typing_state=False) 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)
# !help command # !help command
async def help(self, room_id): async def help(self, room_id):
try: help_info = (
# sending typing state "!gpt [prompt], generate response without context conversation\n"
await self.client.room_typing(room_id) + "!chat [prompt], chat with context conversation\n"
help_info = "!gpt [prompt], generate response without context conversation\n" + \ + "!bing [prompt], chat with context conversation powered by Bing AI\n"
"!chat [prompt], chat with context conversation\n" + \ + "!bard [prompt], chat with Google's Bard\n"
"!bing [prompt], chat with context conversation powered by Bing AI\n" + \ + "!pic [prompt], Image generation by Microsoft Bing\n"
"!bard [prompt], chat with Google's Bard\n" + \ + "!talk [content], talk using chatgpt web (pandora)\n"
"!pic [prompt], Image generation by Microsoft Bing\n" + \ + "!goon, continue the incomplete conversation (pandora)\n"
"!lc [prompt], chat using langchain api\n" + \ + "!new, start a new conversation (pandora)\n"
"!help, help message" # noqa: E501 + "!lc [prompt], chat using langchain api\n"
+ "!help, help message"
) # noqa: E501
await send_room_message(self.client, room_id, reply_message=help_info) await send_room_message(self.client, room_id, reply_message=help_info)
except Exception as e:
logger.error(e, exc_info=True)
# bot login # bot login
async def login(self) -> None: async def login(self) -> None:
@ -702,16 +894,15 @@ class Bot:
# import keys # import keys
async def import_keys(self): async def import_keys(self):
resp = await self.client.import_keys( resp = await self.client.import_keys(
self.import_keys_path, self.import_keys_path, self.import_keys_password
self.import_keys_password
) )
if isinstance(resp, EncryptionError): if isinstance(resp, EncryptionError):
logger.error(f"import_keys failed with {resp}") logger.error(f"import_keys failed with {resp}")
else: else:
logger.info( logger.info(
"import_keys success, please remove import_keys configuration!!!") "import_keys success, please remove import_keys configuration!!!"
)
# sync messages in the room # sync messages in the room
async def sync_forever(self, timeout=30000, full_state=True) -> None: async def sync_forever(self, timeout=30000, full_state=True) -> None:
await self.client.sync_forever(timeout=timeout, full_state=full_state) await self.client.sync_forever(timeout=timeout, full_state=full_state)

View file

@ -2,7 +2,7 @@ services:
app: app:
image: hibobmaster/matrixchatgptbot:latest image: hibobmaster/matrixchatgptbot:latest
container_name: matrix_chatgpt_bot container_name: matrix_chatgpt_bot
restart: always restart: unless-stopped
# build: # build:
# context: . # context: .
# dockerfile: ./Dockerfile # dockerfile: ./Dockerfile
@ -19,13 +19,23 @@ services:
- matrix_network - matrix_network
# api: # api:
# # bing api # # bing api
# image: ghcr.io/waylaidwanderer/node-chatgpt-api:latest # image: hibobmaster/node-chatgpt-api:latest
# container_name: node-chatgpt-api # container_name: node-chatgpt-api
# restart: always # restart: unless-stopped
# volumes: # volumes:
# - ./settings.js:/var/chatgpt-api/settings.js # - ./settings.js:/var/chatgpt-api/settings.js
# networks: # networks:
# - matrix_network # - 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: networks:
matrix_network: matrix_network:

View file

@ -15,5 +15,7 @@
"import_keys_path": "element-keys.txt", "import_keys_path": "element-keys.txt",
"import_keys_password": "xxxxxxxxx", "import_keys_password": "xxxxxxxxx",
"flowise_api_url": "http://localhost:3000/api/v1/prediction/6deb3c89-45bf-4ac4-a0b0-b2d5ef249d21", "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"
} }

81
main.py
View file

@ -9,61 +9,66 @@ logger = getlogger()
async def main(): async def main():
need_import_keys = False need_import_keys = False
if os.path.exists('config.json'): if os.path.exists("config.json"):
fp = open('config.json', 'r', encoding="utf8") fp = open("config.json", "r", encoding="utf8")
config = json.load(fp) config = json.load(fp)
matrix_bot = Bot(homeserver=config.get('homeserver'), matrix_bot = Bot(
user_id=config.get('user_id'), homeserver=config.get("homeserver"),
password=config.get('password'), user_id=config.get("user_id"),
device_id=config.get('device_id'), password=config.get("password"),
room_id=config.get('room_id'), device_id=config.get("device_id"),
api_key=config.get('api_key'), room_id=config.get("room_id"),
bing_api_endpoint=config.get('bing_api_endpoint'), api_key=config.get("api_key"),
access_token=config.get('access_token'), bing_api_endpoint=config.get("bing_api_endpoint"),
bard_token=config.get('bard_token'), access_token=config.get("access_token"),
jailbreakEnabled=config.get('jailbreakEnabled'), bard_token=config.get("bard_token"),
bing_auth_cookie=config.get('bing_auth_cookie'), jailbreakEnabled=config.get("jailbreakEnabled"),
markdown_formatted=config.get('markdown_formatted'), bing_auth_cookie=config.get("bing_auth_cookie"),
output_four_images=config.get('output_four_images'), markdown_formatted=config.get("markdown_formatted"),
import_keys_path=config.get('import_keys_path'), output_four_images=config.get("output_four_images"),
import_keys_password=config.get( import_keys_path=config.get("import_keys_path"),
'import_keys_password'), import_keys_password=config.get("import_keys_password"),
flowise_api_url=config.get('flowise_api_url'), flowise_api_url=config.get("flowise_api_url"),
flowise_api_key=config.get('flowise_api_key'), 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 \ if (
config.get('import_keys_password') is not None: config.get("import_keys_path")
and config.get("import_keys_password") is not None
):
need_import_keys = True need_import_keys = True
else: else:
matrix_bot = Bot(homeserver=os.environ.get('HOMESERVER'), matrix_bot = Bot(
user_id=os.environ.get('USER_ID'), homeserver=os.environ.get("HOMESERVER"),
password=os.environ.get('PASSWORD'), user_id=os.environ.get("USER_ID"),
password=os.environ.get("PASSWORD"),
device_id=os.environ.get("DEVICE_ID"), device_id=os.environ.get("DEVICE_ID"),
room_id=os.environ.get("ROOM_ID"), room_id=os.environ.get("ROOM_ID"),
api_key=os.environ.get("OPENAI_API_KEY"), api_key=os.environ.get("OPENAI_API_KEY"),
bing_api_endpoint=os.environ.get("BING_API_ENDPOINT"), bing_api_endpoint=os.environ.get("BING_API_ENDPOINT"),
access_token=os.environ.get("ACCESS_TOKEN"), access_token=os.environ.get("ACCESS_TOKEN"),
bard_token=os.environ.get("BARD_TOKEN"), bard_token=os.environ.get("BARD_TOKEN"),
jailbreakEnabled=os.environ.get( jailbreakEnabled=os.environ.get("JAILBREAKENABLED", "false").lower()
"JAILBREAKENABLED", "false").lower() \ in ("true", "1", "t"),
in ('true', '1', 't'),
bing_auth_cookie=os.environ.get("BING_AUTH_COOKIE"), bing_auth_cookie=os.environ.get("BING_AUTH_COOKIE"),
markdown_formatted=os.environ.get( markdown_formatted=os.environ.get("MARKDOWN_FORMATTED", "false").lower()
"MARKDOWN_FORMATTED", "false").lower() \ in ("true", "1", "t"),
in ('true', '1', 't'), output_four_images=os.environ.get("OUTPUT_FOUR_IMAGES", "false").lower()
output_four_images=os.environ.get( in ("true", "1", "t"),
"OUTPUT_FOUR_IMAGES", "false").lower() \
in ('true', '1', 't'),
import_keys_path=os.environ.get("IMPORT_KEYS_PATH"), import_keys_path=os.environ.get("IMPORT_KEYS_PATH"),
import_keys_password=os.environ.get( import_keys_password=os.environ.get("IMPORT_KEYS_PASSWORD"),
"IMPORT_KEYS_PASSWORD"),
flowise_api_url=os.environ.get("FLOWISE_API_URL"), flowise_api_url=os.environ.get("FLOWISE_API_URL"),
flowise_api_key=os.environ.get("FLOWISE_API_KEY"), 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") \ if (
and os.environ.get("IMPORT_KEYS_PASSWORD") is not None: os.environ.get("IMPORT_KEYS_PATH")
and os.environ.get("IMPORT_KEYS_PASSWORD") is not None
):
need_import_keys = True need_import_keys = True
await matrix_bot.login() await matrix_bot.login()

106
pandora.py Normal file
View file

@ -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())