diff --git a/.env.example b/.env.example index 8d347cd..9922bbf 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,6 @@ -# Please remove the option that is blank -HOMESERVER="https://matrix.xxxxxx.xxxx" # required +HOMESERVER="https://matrix-client.matrix.org" # required USER_ID="@lullap:xxxxxxxxxxxxx.xxx" # required -PASSWORD="xxxxxxxxxxxxxxx" # Optional -DEVICE_ID="xxxxxxxxxxxxxx" # required +PASSWORD="xxxxxxxxxxxxxxx" # Optional if you use access token +DEVICE_ID="MatrixChatGPTBot" # required ROOM_ID="!FYCmBSkCRUXXXXXXXXX:matrix.XXX.XXX" # Optional, if not set, bot will work on the room it is in OPENAI_API_KEY="xxxxxxxxxxxxxxxxx" # Optional, for !chat and !gpt command -API_ENDPOINT="xxxxxxxxxxxxxxx" # Optional, for !chat and !bing command -ACCESS_TOKEN="xxxxxxxxxxxxxxxxxxxxx" # Optional, use user_id and password is recommended -BARD_TOKEN="xxxxxxxxxxxxxxxxxxxx", # Optional, for !bard command -BING_AUTH_COOKIE="xxxxxxxxxxxxxxxxxxx" # _U cookie, Optional, for Bing Image Creator -MARKDOWN_FORMATTED="true" # Optional -OUTPUT_FOUR_IMAGES="true" # Optional -IMPORT_KEYS_PATH="element-keys.txt" # Optional, used for E2EE Room -IMPORT_KEYS_PASSWORD="xxxxxxx" # Optional -FLOWISE_API_URL="http://localhost:3000/api/v1/prediction/xxxx" # Optional -FLOWISE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxx" # Optional -PANDORA_API_ENDPOINT="http://pandora:8008" # Optional, for !talk, !goon command -PANDORA_API_MODEL="text-davinci-002-render-sha-mobile" # Optional -TEMPERATURE="0.8" # Optional diff --git a/.full-env.example b/.full-env.example new file mode 100644 index 0000000..d1c9f2c --- /dev/null +++ b/.full-env.example @@ -0,0 +1,20 @@ +HOMESERVER="https://matrix-client.matrix.org" +USER_ID="@lullap:xxxxxxxxxxxxx.xxx" +PASSWORD="xxxxxxxxxxxxxxx" +DEVICE_ID="xxxxxxxxxxxxxx" +ROOM_ID="!FYCmBSkCRUXXXXXXXXX:matrix.XXX.XXX" +IMPORT_KEYS_PATH="element-keys.txt" +IMPORT_KEYS_PASSWORD="xxxxxxxxxxxx" +OPENAI_API_KEY="xxxxxxxxxxxxxxxxx" +GPT_API_ENDPOINT="https://api.openai.com/v1/chat/completions" +GPT_MODEL="gpt-3.5-turbo" +MAX_TOKENS=4000 +TOP_P=1.0 +PRESENCE_PENALTY=0.0 +FREQUENCY_PENALTY=0.0 +REPLY_COUNT=1 +SYSTEM_PROMPT="You are ChatGPT, a large language model trained by OpenAI. Respond conversationally" +TEMPERATURE=0.8 +FLOWISE_API_URL="http://flowise:3000/api/v1/prediction/6deb3c89-45bf-4ac4-a0b0-b2d5ef249d21" +FLOWISE_API_KEY="U3pe0bbVDWOyoJtsDzFJjRvHKTP3FRjODwuM78exC3A=" +TIMEOUT=120.0 diff --git a/README.md b/README.md index b591b59..d25e6fb 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,8 @@ pip install -r requirements.txt ``` 3. Create a new config.json file and complete it with the necessary information:
- Use password to login(recommended) or provide `access_token`
If not set:
`room_id`: bot will work in the room where it is in
- `openai_api_key`: `!gpt` `!chat` command will not work
- `api_endpoint`: `!bing` `!chat` command will not work
- `bing_auth_cookie`: `!pic` command will not work ```json { @@ -59,7 +55,7 @@ pip install -r requirements.txt "device_id": "YOUR_DEVICE_ID", "room_id": "YOUR_ROOM_ID", "openai_api_key": "YOUR_API_KEY", - "api_endpoint": "xxxxxxxxx" + "gpt_api_endpoint": "xxxxxxxxx" } ``` diff --git a/compose.yaml b/compose.yaml index bf50a24..e3c67b8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -11,32 +11,13 @@ services: volumes: # use env file or config.json # - ./config.json:/app/config.json - # use touch to create an empty file db, for persist database only - - ./db:/app/db + # use touch to create empty db file, for persist database only + - ./sync_db:/app/sync_db + - ./manage_db:/app/manage_db # import_keys path # - ./element-keys.txt:/app/element-keys.txt networks: - matrix_network - api: - # ChatGPT and Bing API - image: hibobmaster/node-chatgpt-api:latest - container_name: node-chatgpt-api - restart: unless-stopped - volumes: - - ./settings.js:/app/settings.js - networks: - - matrix_network - - # pandora: - # # ChatGPT Web - # 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 56e4365..05f493e 100644 --- a/config.json.sample +++ b/config.json.sample @@ -1,21 +1,7 @@ { - "homeserver": "https://matrix.qqs.tw", + "homeserver": "https://matrix-client.matrix.org", "user_id": "@lullap:xxxxx.org", "password": "xxxxxxxxxxxxxxxxxx", - "device_id": "ECYEOKVPLG", - "room_id": "!FYCmBSkCRUNvZDBaDQ:matrix.qqs.tw", - "openai_api_key": "xxxxxxxxxxxxxxxxxxxxxxxx", - "api_endpoint": "http://api:3000/conversation", - "access_token": "xxxxxxx", - "bard_token": "xxxxxxx", - "bing_auth_cookie": "xxxxxxxxxxx", - "markdown_formatted": true, - "output_four_images": true, - "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=", - "pandora_api_endpoint": "http://127.0.0.1:8008", - "pandora_api_model": "text-davinci-002-render-sha-mobile", - "temperature": 0.8 + "device_id": "MatrixChatGPTBot", + "openai_api_key": "xxxxxxxxxxxxxxxxxxxxxxxx" } diff --git a/full-config.json.sample b/full-config.json.sample new file mode 100644 index 0000000..6d62d4e --- /dev/null +++ b/full-config.json.sample @@ -0,0 +1,22 @@ +{ + "homeserver": "https://matrix-client.matrix.org", + "user_id": "@lullap:xxxxx.org", + "password": "xxxxxxxxxxxxxxxxxx", + "device_id": "MatrixChatGPTBot", + "room_id": "!xxxxxxxxxxxxxxxxxxxxxx:xxxxx.org", + "import_keys_path": "element-keys.txt", + "import_keys_password": "xxxxxxxxxxxxxxxxxxxx", + "openai_api_key": "xxxxxxxxxxxxxxxxxxxxxxxx", + "gpt_api_endpoint": "https://api.openai.com/v1/chat/completions", + "gpt_model": "gpt-3.5-turbo", + "max_tokens": 4000, + "top_p": 1.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0, + "reply_count": 1, + "temperature": 0.8, + "system_prompt": "You are ChatGPT, a large language model trained by OpenAI. Respond conversationally", + "flowise_api_url": "http://flowise:3000/api/v1/prediction/6deb3c89-45bf-4ac4-a0b0-b2d5ef249d21", + "flowise_api_key": "U3pe0bbVDWOyoJtsDzFJjRvHKTP3FRjODwuM78exC3A=", + "timeout": 120.0 +} diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..39a9b58 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,9 @@ +aiofiles +httpx +Markdown +matrix-nio[e2e] +Pillow +tiktoken +tenacity +python-magic +pytest diff --git a/requirements.txt b/requirements.txt index e884258..85bf06f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ aiofiles -aiohttp +httpx Markdown matrix-nio[e2e] Pillow diff --git a/settings.js.example b/settings.js.example deleted file mode 100644 index 57ec272..0000000 --- a/settings.js.example +++ /dev/null @@ -1,101 +0,0 @@ -export default { - // Options for the Keyv cache, see https://www.npmjs.com/package/keyv. - // This is used for storing conversations, and supports additional drivers (conversations are stored in memory by default). - // Only necessary when using `ChatGPTClient`, or `BingAIClient` in jailbreak mode. - cacheOptions: {}, - // If set, `ChatGPTClient` and `BingAIClient` will use `keyv-file` to store conversations to this JSON file instead of in memory. - // However, `cacheOptions.store` will override this if set - storageFilePath: process.env.STORAGE_FILE_PATH || './cache.json', - chatGptClient: { - // Your OpenAI API key (for `ChatGPTClient`) - openaiApiKey: process.env.OPENAI_API_KEY || '', - // (Optional) Support for a reverse proxy for the completions endpoint (private API server). - // Warning: This will expose your `openaiApiKey` to a third party. Consider the risks before using this. - // reverseProxyUrl: 'https://chatgpt.hato.ai/completions', - // (Optional) Parameters as described in https://platform.openai.com/docs/api-reference/completions - modelOptions: { - // You can override the model name and any other parameters here. - // The default model is `gpt-3.5-turbo`. - model: 'gpt-3.5-turbo', - // Set max_tokens here to override the default max_tokens of 1000 for the completion. - // max_tokens: 1000, - }, - // (Optional) Davinci models have a max context length of 4097 tokens, but you may need to change this for other models. - // maxContextTokens: 4097, - // (Optional) You might want to lower this to save money if using a paid model like `text-davinci-003`. - // Earlier messages will be dropped until the prompt is within the limit. - // maxPromptTokens: 3097, - // (Optional) Set custom instructions instead of "You are ChatGPT...". - // (Optional) Set a custom name for the user - // userLabel: 'User', - // (Optional) Set a custom name for ChatGPT ("ChatGPT" by default) - // chatGptLabel: 'Bob', - // promptPrefix: 'You are Bob, a cowboy in Western times...', - // A proxy string like "http://:" - proxy: '', - // (Optional) Set to true to enable `console.debug()` logging - debug: false, - }, - // Options for the Bing client - bingAiClient: { - // Necessary for some people in different countries, e.g. China (https://cn.bing.com) - host: '', - // The "_U" cookie value from bing.com - userToken: '', - // If the above doesn't work, provide all your cookies as a string instead - cookies: '', - // A proxy string like "http://:" - proxy: '', - // (Optional) Set 'x-forwarded-for' for the request. You can use a fixed IPv4 address or specify a range using CIDR notation, - // and the program will randomly select an address within that range. The 'x-forwarded-for' is not used by default now. - // xForwardedFor: '13.104.0.0/14', - // (Optional) Set 'genImage' to true to enable bing to create images for you. It's disabled by default. - // features: { - // genImage: true, - // }, - // (Optional) Set to true to enable `console.debug()` logging - debug: false, - }, - chatGptBrowserClient: { - // (Optional) Support for a reverse proxy for the conversation endpoint (private API server). - // Warning: This will expose your access token to a third party. Consider the risks before using this. - reverseProxyUrl: 'https://bypass.churchless.tech/api/conversation', - // Access token from https://chat.openai.com/api/auth/session - accessToken: '', - // Cookies from chat.openai.com (likely not required if using reverse proxy server). - cookies: '', - // A proxy string like "http://:" - proxy: '', - // (Optional) Set to true to enable `console.debug()` logging - debug: false, - }, - // Options for the API server - apiOptions: { - port: process.env.API_PORT || 3000, - host: process.env.API_HOST || 'localhost', - // (Optional) Set to true to enable `console.debug()` logging - debug: false, - // (Optional) Possible options: "chatgpt", "chatgpt-browser", "bing". (Default: "chatgpt") - // clientToUse: 'bing', - // (Optional) Generate titles for each conversation for clients that support it (only ChatGPTClient for now). - // This will be returned as a `title` property in the first response of the conversation. - generateTitles: false, - // (Optional) Set this to allow changing the client or client options in POST /conversation. - // To disable, set to `null`. - perMessageClientOptionsWhitelist: { - // The ability to switch clients using `clientOptions.clientToUse` will be disabled if `validClientsToUse` is not set. - // To allow switching clients per message, you must set `validClientsToUse` to a non-empty array. - validClientsToUse: ['bing', 'chatgpt'], // values from possible `clientToUse` options above - // The Object key, e.g. "chatgpt", is a value from `validClientsToUse`. - // If not set, ALL options will be ALLOWED to be changed. For example, `bing` is not defined in `perMessageClientOptionsWhitelist` above, - // so all options for `bingAiClient` will be allowed to be changed. - // If set, ONLY the options listed here will be allowed to be changed. - // In this example, each array element is a string representing a property in `chatGptClient` above. - }, - }, - // Options for the CLI app - cliOptions: { - // (Optional) Possible options: "chatgpt", "bing". - // clientToUse: 'bing', - }, -}; diff --git a/src/askgpt.py b/src/askgpt.py deleted file mode 100644 index d3c37ca..0000000 --- a/src/askgpt.py +++ /dev/null @@ -1,45 +0,0 @@ -import json - -import aiohttp -from log import getlogger - -logger = getlogger() - - -class askGPT: - def __init__(self, session: aiohttp.ClientSession): - self.session = session - - async def oneTimeAsk( - self, prompt: str, api_endpoint: str, headers: dict, temperature: float = 0.8 - ) -> str: - jsons = { - "model": "gpt-3.5-turbo", - "messages": [ - { - "role": "user", - "content": prompt, - }, - ], - "temperature": temperature, - } - max_try = 2 - while max_try > 0: - try: - async with self.session.post( - url=api_endpoint, - json=jsons, - headers=headers, - timeout=120, - ) as response: - status_code = response.status - if not status_code == 200: - # print failed reason - logger.warning(str(response.reason)) - max_try = max_try - 1 - continue - - resp = await response.read() - return json.loads(resp)["choices"][0]["message"]["content"] - except Exception as e: - raise Exception(e) diff --git a/src/bot.py b/src/bot.py index ca57f6f..de785ef 100644 --- a/src/bot.py +++ b/src/bot.py @@ -5,9 +5,9 @@ import re import sys import traceback from typing import Union, Optional -import uuid -import aiohttp +import httpx + from nio import ( AsyncClient, AsyncClientConfig, @@ -28,19 +28,15 @@ from nio import ( ) from nio.store.database import SqliteStore -from askgpt import askGPT -from chatgpt_bing import GPTBOT -from BingImageGen import ImageGenAsync from log import getlogger from send_image import send_room_image from send_message import send_room_message -from bard import Bardbot from flowise import flowise_query -from pandora_api import Pandora +from gptbot import Chatbot logger = getlogger() -chatgpt_api_endpoint = "https://api.openai.com/v1/chat/completions" -base_path = Path(os.path.dirname(__file__)).parent +DEVICE_NAME = "MatrixChatGPTBot" +GENERAL_ERROR_MESSAGE = "Something went wrong, please try again or contact admin." class Bot: @@ -48,77 +44,75 @@ class Bot: self, homeserver: str, user_id: str, - device_id: str, - api_endpoint: Optional[str] = None, - openai_api_key: Union[str, None] = None, - temperature: Union[float, None] = None, - room_id: Union[str, None] = None, password: Union[str, None] = None, - access_token: Union[str, None] = None, - bard_token: Union[str, None] = None, - jailbreakEnabled: Union[bool, None] = True, - bing_auth_cookie: Union[str, None] = "", - markdown_formatted: Union[bool, None] = False, - output_four_images: Union[bool, None] = False, + device_id: str = "MatrixChatGPTBot", + room_id: Union[str, None] = None, import_keys_path: Optional[str] = None, import_keys_password: Optional[str] = None, + openai_api_key: Union[str, None] = None, + gpt_api_endpoint: Optional[str] = None, + gpt_model: Optional[str] = None, + max_tokens: Optional[int] = None, + top_p: Optional[float] = None, + presence_penalty: Optional[float] = None, + frequency_penalty: Optional[float] = None, + reply_count: Optional[int] = None, + system_prompt: Optional[str] = None, + temperature: Union[float, None] = None, flowise_api_url: Optional[str] = None, flowise_api_key: Optional[str] = None, - pandora_api_endpoint: Optional[str] = None, - pandora_api_model: Optional[str] = None, + timeout: Union[float, None] = 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: - logger.warning("password or access_toekn is required") + if password is None: + logger.warning("password is required") sys.exit(1) - self.homeserver = homeserver - self.user_id = user_id - self.password = password - self.access_token = access_token - self.bard_token = bard_token - self.device_id = device_id - self.room_id = room_id - self.openai_api_key = openai_api_key - self.bing_auth_cookie = bing_auth_cookie - self.api_endpoint = api_endpoint - self.import_keys_path = import_keys_path - self.import_keys_password = import_keys_password - self.flowise_api_url = flowise_api_url - self.flowise_api_key = flowise_api_key - self.pandora_api_endpoint = pandora_api_endpoint - self.temperature = temperature + self.homeserver: str = homeserver + self.user_id: str = user_id + self.password: str = password + self.device_id: str = device_id + self.room_id: str = room_id - self.session = aiohttp.ClientSession() + self.openai_api_key: str = openai_api_key + self.gpt_api_endpoint: str = ( + gpt_api_endpoint or "https://api.openai.com/v1/chat/completions" + ) + self.gpt_model: str = gpt_model or "gpt-3.5-turbo" + self.max_tokens: int = max_tokens or 4000 + self.top_p: float = top_p or 1.0 + self.temperature: float = temperature or 0.8 + self.presence_penalty: float = presence_penalty or 0.0 + self.frequency_penalty: float = frequency_penalty or 0.0 + self.reply_count: int = reply_count or 1 + self.system_prompt: str = ( + system_prompt + or "You are ChatGPT, \ + a large language model trained by OpenAI. Respond conversationally" + ) - if openai_api_key is not None: - if not self.openai_api_key.startswith("sk-"): - logger.warning("invalid openai api key") - sys.exit(1) + self.import_keys_path: str = import_keys_path + self.import_keys_password: str = import_keys_password + self.flowise_api_url: str = flowise_api_url + self.flowise_api_key: str = flowise_api_key - if jailbreakEnabled is None: - self.jailbreakEnabled = True - else: - self.jailbreakEnabled = jailbreakEnabled + self.timeout: float = timeout or 120.0 - if markdown_formatted is None: - self.markdown_formatted = False - else: - self.markdown_formatted = markdown_formatted + self.base_path = Path(os.path.dirname(__file__)).parent - if output_four_images is None: - self.output_four_images = False - else: - self.output_four_images = output_four_images + self.httpx_client = httpx.AsyncClient( + follow_redirects=True, + timeout=self.timeout, + ) # initialize AsyncClient object - self.store_path = base_path + self.store_path = self.base_path self.config = AsyncClientConfig( store=SqliteStore, - store_name="db", + store_name="sync_db", store_sync_tokens=True, encryption_enabled=True, ) @@ -130,8 +124,21 @@ class Bot: store_path=self.store_path, ) - if self.access_token is not None: - self.client.access_token = self.access_token + # initialize Chatbot object + self.chatbot = Chatbot( + aclient=self.httpx_client, + api_key=self.openai_api_key, + api_url=self.gpt_api_endpoint, + engine=self.gpt_model, + timeout=self.timeout, + max_tokens=self.max_tokens, + top_p=self.top_p, + presence_penalty=self.presence_penalty, + frequency_penalty=self.frequency_penalty, + reply_count=self.reply_count, + system_prompt=self.system_prompt, + temperature=self.temperature, + ) # setup event callbacks self.client.add_event_callback(self.message_callback, (RoomMessageText,)) @@ -144,81 +151,22 @@ class Bot: # regular expression to match keyword commands 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.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 askGPT class - self.askgpt = askGPT(self.session) - # request header for !gpt command - self.gptheaders = { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.openai_api_key}", - } - - # initialize bing and chatgpt - if self.api_endpoint is not None: - self.gptbot = GPTBOT(self.api_endpoint, self.session) - self.chatgpt_data = {} - self.bing_data = {} - - # initialize BingImageGenAsync - if self.bing_auth_cookie != "": - self.imageGen = ImageGenAsync(self.bing_auth_cookie, quiet=True) - - # initialize pandora - if pandora_api_endpoint is not None: - 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 = {} - - # initialize bard - self.bard_data = {} - - def __del__(self): - try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete(self._close()) - - async def _close(self): - await self.session.close() + async def close(self, task: asyncio.Task) -> None: + await self.httpx_client.aclose() + await self.client.close() + task.cancel() + logger.info("Bot closed!") def chatgpt_session_init(self, sender_id: str) -> None: self.chatgpt_data[sender_id] = { "first_time": True, } - def bing_session_init(self, sender_id: str) -> None: - self.bing_data[sender_id] = { - "first_time": True, - } - - def pandora_session_init(self, sender_id: str) -> None: - self.pandora_data[sender_id] = { - "conversation_id": None, - "parent_message_id": str(uuid.uuid4()), - "first_time": True, - } - - async def bard_session_init(self, sender_id: str) -> None: - self.bard_data[sender_id] = { - "instance": await Bardbot.create(self.bard_token, 60), - } - # message_callback RoomMessageText event async def message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None: if self.room_id is None: @@ -267,7 +215,7 @@ class Bot: except Exception as e: logger.error(e, exc_info=True) - if self.api_endpoint is not None: + if self.gpt_api_endpoint is not None: # chatgpt n = self.chat_prog.match(content_body) if n: @@ -293,58 +241,6 @@ class Bot: self.client, room_id, reply_message="API_KEY not provided" ) - # bing ai - # if self.bing_api_endpoint != "": - # bing ai can be used without cookie - b = self.bing_prog.match(content_body) - if b: - if sender_id not in self.bing_data: - self.bing_session_init(sender_id) - 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, - ) - ) - except Exception as e: - logger.error(e, exc_info=True) - - # Image Generation by Microsoft Bing - if self.bing_auth_cookie != "": - i = self.pic_prog.match(content_body) - if i: - prompt = i.group(1) - try: - asyncio.create_task(self.pic(room_id, prompt)) - except Exception as e: - logger.error(e, exc_info=True) - - # Google's Bard - if self.bard_token is not None: - if sender_id not in self.bard_data: - await self.bard_session_init(sender_id) - b = self.bard_prog.match(content_body) - if b: - prompt = b.group(1) - try: - 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) - # lc command if self.flowise_api_url is not None: m = self.lc_prog.match(content_body) @@ -364,46 +260,10 @@ class Bot: await send_room_message(self.client, room_id, reply_message={e}) logger.error(e, exc_info=True) - # pandora - if self.pandora_api_endpoint is not None: - t = self.talk_prog.match(content_body) - if t: - if sender_id not in self.pandora_data: - self.pandora_session_init(sender_id) - 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) - - g = self.goon_prog.match(content_body) - if g: - if sender_id not in self.pandora_data: - self.pandora_session_init(sender_id) - 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) - # !new command n = self.new_prog.match(content_body) if n: - new_command_kind = n.group(1) + new_command = n.group(1) try: asyncio.create_task( self.new( @@ -411,7 +271,7 @@ class Bot: reply_to_event_id, sender_id, raw_user_message, - new_command_kind, + new_command, ) ) except Exception as e: @@ -421,7 +281,11 @@ class Bot: h = self.help_prog.match(content_body) if h: try: - asyncio.create_task(self.help(room_id)) + asyncio.create_task( + self.help( + room_id, reply_to_event_id, sender_id, raw_user_message + ) + ) except Exception as e: logger.error(e, exc_info=True) @@ -670,7 +534,7 @@ class Bot: self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message ): try: - await self.client.room_typing(room_id, timeout=300000) + await self.client.room_typing(room_id, timeout=int(self.timeout) * 1000) if ( self.chatgpt_data[sender_id]["first_time"] or "conversationId" not in self.chatgpt_data[sender_id] @@ -705,128 +569,43 @@ class Bot: self.client, room_id, reply_message=content, - reply_to_event_id="", + reply_to_event_id=reply_to_event_id, sender_id=sender_id, user_message=raw_user_message, - markdown_formatted=self.markdown_formatted, ) - except Exception as e: - await send_room_message(self.client, room_id, reply_message=str(e)) + except Exception: + await send_room_message( + self.client, + room_id, + reply_message=GENERAL_ERROR_MESSAGE, + reply_to_event_id=reply_to_event_id, + ) # !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=30000) - # timeout 300s - text = await asyncio.wait_for( - self.askgpt.oneTimeAsk( - prompt, chatgpt_api_endpoint, self.gptheaders, self.temperature - ), - timeout=300, + # sending typing state, seconds to milliseconds + await self.client.room_typing(room_id, timeout=int(self.timeout) * 1000) + responseMessage = await self.chatbot.oneTimeAsk( + prompt=prompt, ) - text = text.strip() await send_room_message( self.client, room_id, - reply_message=text, - reply_to_event_id="", + reply_message=responseMessage.strip(), + reply_to_event_id=reply_to_event_id, sender_id=sender_id, user_message=raw_user_message, - markdown_formatted=self.markdown_formatted, ) except Exception: await send_room_message( self.client, room_id, - reply_message="Error encountered, please try again or contact admin.", - ) - - # !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=300000) - - if ( - self.bing_data[sender_id]["first_time"] - or "conversationId" not in self.bing_data[sender_id] - ): - self.bing_data[sender_id]["first_time"] = False - payload = { - "message": prompt, - "clientOptions": { - "clientToUse": "bing", - }, - } - else: - payload = { - "message": prompt, - "clientOptions": { - "clientToUse": "bing", - }, - "conversationSignature": self.bing_data[sender_id][ - "conversationSignature" - ], - "conversationId": self.bing_data[sender_id]["conversationId"], - "clientId": self.bing_data[sender_id]["clientId"], - "invocationId": self.bing_data[sender_id]["invocationId"], - } - resp = await self.gptbot.queryBing(payload) - content = "".join( - [body["text"] for body in resp["details"]["adaptiveCards"][0]["body"]] - ) - self.bing_data[sender_id]["conversationSignature"] = resp[ - "conversationSignature" - ] - self.bing_data[sender_id]["conversationId"] = resp["conversationId"] - self.bing_data[sender_id]["clientId"] = resp["clientId"] - self.bing_data[sender_id]["invocationId"] = resp["invocationId"] - - text = content.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: - await send_room_message(self.client, room_id, reply_message=str(e)) - - # !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 self.bard_data[sender_id]["instance"].ask(prompt) - - 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 TimeoutError: - await send_room_message(self.client, room_id, reply_message="TimeoutError") - except Exception: - await send_room_message( - self.client, - room_id, - reply_message="Error calling Bard API, please contact admin.", + reply_message=GENERAL_ERROR_MESSAGE, + reply_to_event_id=reply_to_event_id, ) # !lc command @@ -835,120 +614,32 @@ class Bot: ) -> None: try: # sending typing state - await self.client.room_typing(room_id) + await self.client.room_typing(room_id, timeout=int(self.timeout) * 1000) if self.flowise_api_key is not None: headers = {"Authorization": f"Bearer {self.flowise_api_key}"} - response = await flowise_query( - self.flowise_api_url, prompt, self.session, headers + responseMessage = await flowise_query( + self.flowise_api_url, prompt, self.httpx_client, headers ) else: - response = await flowise_query( - self.flowise_api_url, prompt, self.session + responseMessage = await flowise_query( + self.flowise_api_url, prompt, self.httpx_client ) await send_room_message( self.client, room_id, - reply_message=response, - reply_to_event_id="", + reply_message=responseMessage.strip(), + reply_to_event_id=reply_to_event_id, sender_id=sender_id, user_message=raw_user_message, - markdown_formatted=self.markdown_formatted, ) except Exception: await send_room_message( self.client, room_id, - reply_message="Error calling flowise API, please contact admin.", + reply_message=GENERAL_ERROR_MESSAGE, + reply_to_event_id=reply_to_event_id, ) - # !talk command - async def talk( - self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message - ) -> None: - try: - 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, - ) - except Exception as e: - await send_room_message(self.client, room_id, reply_message=str(e)) - - # !goon command - async def goon( - self, room_id, reply_to_event_id, sender_id, raw_user_message - ) -> None: - try: - # 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, - ) - except Exception as e: - await send_room_message(self.client, room_id, reply_message=str(e)) - # !new command async def new( self, @@ -956,29 +647,14 @@ class Bot: reply_to_event_id, sender_id, raw_user_message, - new_command_kind, + new_command, ) -> None: try: - if "talk" in new_command_kind: - self.pandora_session_init(sender_id) - content = ( - "New conversation created, please use !talk to start chatting!" - ) - elif "chat" in new_command_kind: + if "chat" in new_command: self.chatgpt_session_init(sender_id) content = ( "New conversation created, please use !chat to start chatting!" ) - elif "bing" in new_command_kind: - self.bing_session_init(sender_id) - content = ( - "New conversation created, please use !bing to start chatting!" - ) - elif "bard" in new_command_kind: - await self.bard_session_init(sender_id) - content = ( - "New conversation created, please use !bard to start chatting!" - ) else: content = "Unkown keyword, please use !help to see the usage!" @@ -986,32 +662,41 @@ class Bot: self.client, room_id, reply_message=content, - reply_to_event_id="", + reply_to_event_id=reply_to_event_id, sender_id=sender_id, user_message=raw_user_message, - markdown_formatted=self.markdown_formatted, ) - except Exception as e: - await send_room_message(self.client, room_id, reply_message=str(e)) + except Exception: + await send_room_message( + self.client, + room_id, + reply_message=GENERAL_ERROR_MESSAGE, + reply_to_event_id=reply_to_event_id, + ) # !pic command - async def pic(self, room_id, prompt): + async def pic(self, room_id, prompt, replay_to_event_id): try: - await self.client.room_typing(room_id, timeout=300000) + await self.client.room_typing(room_id, timeout=int(self.timeout) * 1000) # generate image links = await self.imageGen.get_images(prompt) image_path_list = await self.imageGen.save_images( - links, base_path / "images", self.output_four_images + links, self.base_path / "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) except Exception as e: - await send_room_message(self.client, room_id, reply_message=str(e)) + await send_room_message( + self.client, + room_id, + reply_message=str(e), + reply_to_event_id=replay_to_event_id, + ) # !help command - async def help(self, room_id): + async def help(self, room_id, reply_to_event_id, sender_id, user_message): help_info = ( "!gpt [prompt], generate a one time response without context conversation\n" + "!chat [prompt], chat with context conversation\n" @@ -1025,21 +710,24 @@ class Bot: + "!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, + sender_id=sender_id, + user_message=user_message, + reply_to_event_id=reply_to_event_id, + ) # bot login async def login(self) -> None: - if self.access_token is not None: - logger.info("Login via access_token") - else: - logger.info("Login via password") - try: - resp = await self.client.login(password=self.password) - if not isinstance(resp, LoginResponse): - logger.error("Login Failed") - sys.exit(1) - except Exception as e: - logger.error(f"Error: {e}", exc_info=True) + resp = await self.client.login(password=self.password, device_name=DEVICE_NAME) + if not isinstance(resp, LoginResponse): + logger.error("Login Failed") + await self.httpx_client.aclose() + await self.client.close() + sys.exit(1) + logger.info("Success login via password") # import keys async def import_keys(self): diff --git a/src/flowise.py b/src/flowise.py index 500dbf6..a4a99b2 100644 --- a/src/flowise.py +++ b/src/flowise.py @@ -1,8 +1,8 @@ -import aiohttp +import httpx async def flowise_query( - api_url: str, prompt: str, session: aiohttp.ClientSession, headers: dict = None + api_url: str, prompt: str, session: httpx.AsyncClient, headers: dict = None ) -> str: """ Sends a query to the Flowise API and returns the response. @@ -24,17 +24,15 @@ async def flowise_query( ) else: response = await session.post(api_url, json={"question": prompt}) - return await response.json() + return await response.text() async def test(): - session = aiohttp.ClientSession() - api_url = ( - "http://127.0.0.1:3000/api/v1/prediction/683f9ea8-e670-4d51-b657-0886eab9cea1" - ) - prompt = "What is the capital of France?" - response = await flowise_query(api_url, prompt, session) - print(response) + async with httpx.AsyncClient() as session: + api_url = "http://127.0.0.1:3000/api/v1/prediction/683f9ea8-e670-4d51-b657-0886eab9cea1" + prompt = "What is the capital of France?" + response = await flowise_query(api_url, prompt, session) + print(response) if __name__ == "__main__": diff --git a/src/gptbot.py b/src/gptbot.py new file mode 100644 index 0000000..8750cd5 --- /dev/null +++ b/src/gptbot.py @@ -0,0 +1,292 @@ +""" +Code derived from https://github.com/acheong08/ChatGPT/blob/main/src/revChatGPT/V3.py +A simple wrapper for the official ChatGPT API +""" +import json +from typing import AsyncGenerator +from tenacity import retry, stop_after_attempt, wait_random_exponential + +import httpx +import tiktoken + + +ENGINES = [ + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-16k-0613", + "gpt-4", + "gpt-4-32k", + "gpt-4-0613", + "gpt-4-32k-0613", +] + + +class Chatbot: + """ + Official ChatGPT API + """ + + def __init__( + self, + aclient: httpx.AsyncClient, + api_key: str, + api_url: str = None, + engine: str = None, + timeout: float = None, + max_tokens: int = None, + temperature: float = 0.8, + top_p: float = 1.0, + presence_penalty: float = 0.0, + frequency_penalty: float = 0.0, + reply_count: int = 1, + truncate_limit: int = None, + system_prompt: str = None, + ) -> None: + """ + Initialize Chatbot with API key (from https://platform.openai.com/account/api-keys) + """ + self.engine: str = engine or "gpt-3.5-turbo" + self.api_key: str = api_key + self.api_url: str = api_url or "https://api.openai.com/v1/chat/completions" + self.system_prompt: str = ( + system_prompt + or "You are ChatGPT, \ + a large language model trained by OpenAI. Respond conversationally" + ) + self.max_tokens: int = max_tokens or ( + 31000 + if "gpt-4-32k" in engine + else 7000 + if "gpt-4" in engine + else 15000 + if "gpt-3.5-turbo-16k" in engine + else 4000 + ) + self.truncate_limit: int = truncate_limit or ( + 30500 + if "gpt-4-32k" in engine + else 6500 + if "gpt-4" in engine + else 14500 + if "gpt-3.5-turbo-16k" in engine + else 3500 + ) + self.temperature: float = temperature + self.top_p: float = top_p + self.presence_penalty: float = presence_penalty + self.frequency_penalty: float = frequency_penalty + self.reply_count: int = reply_count + self.timeout: float = timeout + + self.aclient = aclient + + self.conversation: dict[str, list[dict]] = { + "default": [ + { + "role": "system", + "content": system_prompt, + }, + ], + } + + if self.get_token_count("default") > self.max_tokens: + raise Exception("System prompt is too long") + + def add_to_conversation( + self, + message: str, + role: str, + convo_id: str = "default", + ) -> None: + """ + Add a message to the conversation + """ + self.conversation[convo_id].append({"role": role, "content": message}) + + def __truncate_conversation(self, convo_id: str = "default") -> None: + """ + Truncate the conversation + """ + while True: + if ( + self.get_token_count(convo_id) > self.truncate_limit + and len(self.conversation[convo_id]) > 1 + ): + # Don't remove the first message + self.conversation[convo_id].pop(1) + else: + break + + # https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb + def get_token_count(self, convo_id: str = "default") -> int: + """ + Get token count + """ + if self.engine not in ENGINES: + raise NotImplementedError( + f"Engine {self.engine} is not supported. Select from {ENGINES}", + ) + tiktoken.model.MODEL_TO_ENCODING["gpt-4"] = "cl100k_base" + + encoding = tiktoken.encoding_for_model(self.engine) + + num_tokens = 0 + for message in self.conversation[convo_id]: + # every message follows {role/name}\n{content}\n + num_tokens += 5 + for key, value in message.items(): + if value: + num_tokens += len(encoding.encode(value)) + if key == "name": # if there's a name, the role is omitted + num_tokens += 5 # role is always required and always 1 token + num_tokens += 5 # every reply is primed with assistant + return num_tokens + + def get_max_tokens(self, convo_id: str) -> int: + """ + Get max tokens + """ + return self.max_tokens - self.get_token_count(convo_id) + + async def ask_stream_async( + self, + prompt: str, + role: str = "user", + convo_id: str = "default", + model: str = None, + pass_history: bool = True, + **kwargs, + ) -> AsyncGenerator[str, None]: + """ + Ask a question + """ + # Make conversation if it doesn't exist + if convo_id not in self.conversation: + self.reset(convo_id=convo_id, system_prompt=self.system_prompt) + self.add_to_conversation(prompt, "user", convo_id=convo_id) + self.__truncate_conversation(convo_id=convo_id) + # Get response + async with self.aclient.stream( + "post", + self.api_url, + headers={"Authorization": f"Bearer {kwargs.get('api_key', self.api_key)}"}, + json={ + "model": model or self.engine, + "messages": self.conversation[convo_id] if pass_history else [prompt], + "stream": True, + # kwargs + "temperature": kwargs.get("temperature", self.temperature), + "top_p": kwargs.get("top_p", self.top_p), + "presence_penalty": kwargs.get( + "presence_penalty", + self.presence_penalty, + ), + "frequency_penalty": kwargs.get( + "frequency_penalty", + self.frequency_penalty, + ), + "n": kwargs.get("n", self.reply_count), + "user": role, + "max_tokens": min( + self.get_max_tokens(convo_id=convo_id), + kwargs.get("max_tokens", self.max_tokens), + ), + }, + timeout=kwargs.get("timeout", self.timeout), + ) as response: + if response.status_code != 200: + await response.aread() + raise Exception( + f"{response.status_code} {response.reason_phrase} {response.text}", + ) + + response_role: str = "" + full_response: str = "" + async for line in response.aiter_lines(): + line = line.strip() + if not line: + continue + # Remove "data: " + line = line[6:] + if line == "[DONE]": + break + resp: dict = json.loads(line) + if "error" in resp: + raise Exception(f"{resp['error']}") + choices = resp.get("choices") + if not choices: + continue + delta: dict[str, str] = choices[0].get("delta") + if not delta: + continue + if "role" in delta: + response_role = delta["role"] + if "content" in delta: + content: str = delta["content"] + full_response += content + yield content + self.add_to_conversation(full_response, response_role, convo_id=convo_id) + + async def ask_async( + self, + prompt: str, + role: str = "user", + convo_id: str = "default", + model: str = None, + pass_history: bool = True, + **kwargs, + ) -> str: + """ + Non-streaming ask + """ + response = self.ask_stream_async( + prompt=prompt, + role=role, + convo_id=convo_id, + model=model, + pass_history=pass_history, + **kwargs, + ) + full_response: str = "".join([r async for r in response]) + return full_response + + def reset(self, convo_id: str = "default", system_prompt: str = None) -> None: + """ + Reset the conversation + """ + self.conversation[convo_id] = [ + {"role": "system", "content": system_prompt or self.system_prompt}, + ] + + @retry(wait=wait_random_exponential(min=2, max=5), stop=stop_after_attempt(3)) + async def oneTimeAsk( + self, + prompt: str, + role: str = "user", + model: str = None, + **kwargs, + ) -> str: + async with self.aclient.post( + url=self.api_url, + json={ + "model": model or self.engine, + "messages": prompt, + # kwargs + "temperature": kwargs.get("temperature", self.temperature), + "top_p": kwargs.get("top_p", self.top_p), + "presence_penalty": kwargs.get( + "presence_penalty", + self.presence_penalty, + ), + "frequency_penalty": kwargs.get( + "frequency_penalty", + self.frequency_penalty, + ), + "user": role, + }, + headers={"Authorization": f"Bearer {kwargs.get('api_key', self.api_key)}"}, + timeout=kwargs.get("timeout", self.timeout), + ) as response: + resp = await response.read() + return json.loads(resp)["choices"][0]["message"]["content"] diff --git a/src/main.py b/src/main.py index 28940ce..fef7d57 100644 --- a/src/main.py +++ b/src/main.py @@ -2,6 +2,8 @@ import asyncio import json import os from pathlib import Path +import signal +import sys from bot import Bot from log import getlogger @@ -13,8 +15,12 @@ async def main(): need_import_keys = False config_path = Path(os.path.dirname(__file__)).parent / "config.json" if os.path.isfile(config_path): - fp = open(config_path, encoding="utf8") - config = json.load(fp) + try: + fp = open(config_path, encoding="utf8") + config = json.load(fp) + except Exception: + logger.error("config.json load error, please check the file") + sys.exit(1) matrix_bot = Bot( homeserver=config.get("homeserver"), @@ -22,21 +28,21 @@ async def main(): password=config.get("password"), device_id=config.get("device_id"), room_id=config.get("room_id"), - openai_api_key=config.get("openai_api_key"), - api_endpoint=config.get("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"), + openai_api_key=config.get("openai_api_key"), + gpt_api_endpoint=config.get("gpt_api_endpoint"), + gpt_model=config.get("gpt_model"), + max_tokens=int(config.get("max_tokens")), + top_p=float(config.get("top_p")), + presence_penalty=float(config.get("presence_penalty")), + frequency_penalty=float(config.get("frequency_penalty")), + reply_count=int(config.get("reply_count")), + system_prompt=config.get("system_prompt"), + temperature=float(config.get("temperature")), 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"), - temperature=float(config.get("temperature", 0.8)), + timeout=float(config.get("timeout")), ) if ( config.get("import_keys_path") @@ -51,24 +57,21 @@ async def main(): password=os.environ.get("PASSWORD"), device_id=os.environ.get("DEVICE_ID"), room_id=os.environ.get("ROOM_ID"), - openai_api_key=os.environ.get("OPENAI_API_KEY"), - api_endpoint=os.environ.get("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"), + openai_api_key=os.environ.get("OPENAI_API_KEY"), + gpt_api_endpoint=os.environ.get("GPT_API_ENDPOINT"), + gpt_model=os.environ.get("GPT_MODEL"), + max_tokens=int(os.environ.get("MAX_TOKENS")), + top_p=float(os.environ.get("TOP_P")), + presence_penalty=float(os.environ.get("PRESENCE_PENALTY")), + frequency_penalty=float(os.environ.get("FREQUENCY_PENALTY")), + reply_count=int(os.environ.get("REPLY_COUNT")), + system_prompt=os.environ.get("SYSTEM_PROMPT"), + temperature=float(os.environ.get("TEMPERATURE")), 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"), - temperature=float(os.environ.get("TEMPERATURE", 0.8)), + timeout=float(os.environ.get("TIMEOUT")), ) if ( os.environ.get("IMPORT_KEYS_PATH") @@ -80,7 +83,20 @@ async def main(): if need_import_keys: logger.info("start import_keys process, this may take a while...") await matrix_bot.import_keys() - await matrix_bot.sync_forever(timeout=30000, full_state=True) + + sync_task = asyncio.create_task( + matrix_bot.sync_forever(timeout=30000, full_state=True) + ) + + # handle signal interrupt + loop = asyncio.get_running_loop() + for signame in ("SIGINT", "SIGTERM"): + loop.add_signal_handler( + getattr(signal, signame), + lambda: asyncio.create_task(matrix_bot.close(sync_task)), + ) + + await sync_task if __name__ == "__main__": diff --git a/src/pandora_api.py b/src/pandora_api.py deleted file mode 100644 index 4b4d1c5..0000000 --- a/src/pandora_api.py +++ /dev/null @@ -1,111 +0,0 @@ -# API wrapper for https://github.com/pengzhile/pandora/blob/master/doc/HTTP-API.md -import asyncio -import uuid - -import aiohttp - - -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()) diff --git a/src/send_message.py b/src/send_message.py index 946360b..26179d6 100644 --- a/src/send_message.py +++ b/src/send_message.py @@ -1,5 +1,3 @@ -import re - import markdown from log import getlogger from nio import AsyncClient @@ -14,32 +12,19 @@ async def send_room_message( sender_id: str = "", user_message: str = "", reply_to_event_id: str = "", - markdown_formatted: bool = False, ) -> None: - NORMAL_BODY = content = { - "msgtype": "m.text", - "body": reply_message, - } if reply_to_event_id == "": - if markdown_formatted: - # only format message contains multiline codes, *, | - if re.search(r"```|\*|\|", reply_message) is not None: - content = { - "msgtype": "m.text", - "body": reply_message, - "format": "org.matrix.custom.html", - "formatted_body": markdown.markdown( - reply_message, - extensions=["nl2br", "tables", "fenced_code"], - ), - } - else: - content = NORMAL_BODY - - else: - content = NORMAL_BODY + content = { + "msgtype": "m.text", + "body": reply_message, + "format": "org.matrix.custom.html", + "formatted_body": markdown.markdown( + reply_message, + extensions=["nl2br", "tables", "fenced_code"], + ), + } else: - body = r"> <" + sender_id + r"> " + user_message + r"\n\n" + reply_message + body = "> <" + sender_id + "> " + user_message + "\n\n" + reply_message format = r"org.matrix.custom.html" formatted_body = ( r'

" + user_message + r"
" - + reply_message + + markdown.markdown( + reply_message, + extensions=["nl2br", "tables", "fenced_code"], + ) ) content = { diff --git a/sync_db b/sync_db new file mode 100644 index 0000000..27d67de Binary files /dev/null and b/sync_db differ