Compare commits

..

No commits in common. "main" and "v1.0.6" have entirely different histories.
main ... v1.0.6

32 changed files with 1580 additions and 2405 deletions

View file

@ -1,6 +1,18 @@
HOMESERVER="https://matrix-client.matrix.org" # required # Please remove the option that is blank
HOMESERVER="https://matrix.xxxxxx.xxxx" # required
USER_ID="@lullap:xxxxxxxxxxxxx.xxx" # required USER_ID="@lullap:xxxxxxxxxxxxx.xxx" # required
PASSWORD="xxxxxxxxxxxxxxx" # required PASSWORD="xxxxxxxxxxxxxxx" # Optional
DEVICE_ID="MatrixChatGPTBot" # required DEVICE_ID="xxxxxxxxxxxxxx" # required
ROOM_ID="!FYCmBSkCRUXXXXXXXXX:matrix.XXX.XXX" # Optional, if not set, bot will work on the room it is in ROOM_ID="!FYCmBSkCRUXXXXXXXXX:matrix.XXX.XXX" # Optional, if not set, bot will work on the room it is in
OPENAI_API_KEY="xxxxxxxxxxxxxxxxx" # Optional OPENAI_API_KEY="xxxxxxxxxxxxxxxxx" # Optional, for !chat and !gpt command
BING_API_ENDPOINT="xxxxxxxxxxxxxxx" # Optional, for !bing command
ACCESS_TOKEN="xxxxxxxxxxxxxxxxxxxxx" # Optional, use user_id and password is recommended
BARD_TOKEN="xxxxxxxxxxxxxxxxxxxx", # Optional, for !bard command
JAILBREAKENABLED="true" # Optional
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
IMPORT_KEYS_PASSWORD="xxxxxxx" # Optional
FLOWISE_API_URL="http://localhost:3000/api/v1/prediction/xxxx" # Optional
FLOWISE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxx" # Optional

View file

@ -1,27 +0,0 @@
HOMESERVER="https://matrix-client.matrix.org"
USER_ID="@lullap:xxxxxxxxxxxxx.xxx"
PASSWORD="xxxxxxxxxxxxxxx"
ACCESS_TOKEN="xxxxxxxxxxx"
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
LC_ADMIN="@admin:xxxxxx.xxx,@admin2:xxxxxx.xxx"
IMAGE_GENERATION_ENDPOINT="http://127.0.0.1:7860/sdapi/v1/txt2img"
IMAGE_GENERATION_BACKEND="sdwui" # openai or sdwui or localai
IMAGE_GENERATION_SIZE="512x512"
IMAGE_FORMAT="webp"
SDWUI_STEPS=20
SDWUI_SAMPLER_NAME="Euler a"
SDWUI_CFG_SCALE=7
TIMEOUT=120.0

28
.github/workflows/pylint.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Pylint
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- name: Install libolm-dev
run: |
sudo apt install -y libolm-dev
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
pip install -U pip setuptools wheel
pip install -r requirements.txt
pip install pylint
- name: Analysing the code with pylint
run: |
pylint $(git ls-files '*.py') --errors-only

5
.gitignore vendored
View file

@ -168,8 +168,3 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/ .idea/
# Custom
sync_db
manage_db
element-keys.txt

View file

@ -1,16 +0,0 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/psf/black
rev: 23.12.0
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.7
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]

3
.vscode/settings.json vendored Normal file
View file

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

175
BingImageGen.py Normal file
View file

@ -0,0 +1,175 @@
"""
Code derived from:
https://github.com/acheong08/EdgeGPT/blob/f940cecd24a4818015a8b42a2443dd97c3c2a8f4/src/ImageGen.py
"""
from log import getlogger
from uuid import uuid4
import os
import contextlib
import aiohttp
import asyncio
import random
import requests
import regex
logger = getlogger()
BING_URL = "https://www.bing.com"
# Generate random IP between range 13.104.0.0/14
FORWARDED_IP = (
f"13.{random.randint(104, 107)}.{random.randint(0, 255)}.{random.randint(0, 255)}"
)
HEADERS = {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", # noqa: E501
"accept-language": "en-US,en;q=0.9",
"cache-control": "max-age=0",
"content-type": "application/x-www-form-urlencoded",
"referrer": "https://www.bing.com/images/create/",
"origin": "https://www.bing.com",
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63", # noqa: E501
"x-forwarded-for": FORWARDED_IP,
}
class ImageGenAsync:
"""
Image generation by Microsoft Bing
Parameters:
auth_cookie: str
"""
def __init__(self, auth_cookie: str, quiet: bool = True) -> None:
self.session = aiohttp.ClientSession(
headers=HEADERS,
cookies={"_U": auth_cookie},
)
self.quiet = quiet
async def __aenter__(self):
return self
async def __aexit__(self, *excinfo) -> None:
await self.session.close()
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 get_images(self, prompt: str) -> list:
"""
Fetches image links from Bing
Parameters:
prompt: str
"""
if not self.quiet:
print("Sending request...")
url_encoded_prompt = requests.utils.quote(prompt)
# https://www.bing.com/images/create?q=<PROMPT>&rt=3&FORM=GENCRE
url = f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=4&FORM=GENCRE"
async with self.session.post(url, allow_redirects=False) as response:
content = await response.text()
if "this prompt has been blocked" in content.lower():
raise Exception(
"Your prompt has been blocked by Bing. Try to change any bad words and try again.", # noqa: E501
)
if response.status != 302:
# if rt4 fails, try rt3
url = (
f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=3&FORM=GENCRE"
)
async with self.session.post(
url,
allow_redirects=False,
timeout=200,
) as response3:
if response3.status != 302:
print(f"ERROR: {response3.text}")
raise Exception("Redirect failed")
response = response3
# Get redirect URL
redirect_url = response.headers["Location"].replace("&nfy=1", "")
request_id = redirect_url.split("id=")[-1]
await self.session.get(f"{BING_URL}{redirect_url}")
# https://www.bing.com/images/create/async/results/{ID}?q={PROMPT}
polling_url = f"{BING_URL}/images/create/async/results/{request_id}?q={url_encoded_prompt}" # noqa: E501
# Poll for results
if not self.quiet:
print("Waiting for results...")
while True:
if not self.quiet:
print(".", end="", flush=True)
# By default, timeout is 300s, change as needed
response = await self.session.get(polling_url)
if response.status != 200:
raise Exception("Could not get results")
content = await response.text()
if content and content.find("errorMessage") == -1:
break
await asyncio.sleep(1)
continue
# Use regex to search for src=""
image_links = regex.findall(r'src="([^"]+)"', content)
# Remove size limit
normal_image_links = [link.split("?w=")[0] for link in image_links]
# Remove duplicates
normal_image_links = list(set(normal_image_links))
# Bad images
bad_images = [
"https://r.bing.com/rp/in-2zU3AJUdkgFe7ZKv19yPBHVs.png",
"https://r.bing.com/rp/TX9QuO3WzcCJz1uaaSwQAz39Kb0.jpg",
]
for im in normal_image_links:
if im in bad_images:
raise Exception("Bad images")
# No images
if not normal_image_links:
raise Exception("No images")
return normal_image_links
async def save_images(self, links: list, output_dir: str,
output_four_images: bool) -> list:
"""
Saves images to output directory
"""
with contextlib.suppress(FileExistsError):
os.mkdir(output_dir)
image_path_list = []
if output_four_images:
for link in links:
image_name = str(uuid4())
image_path = os.path.join(output_dir, f"{image_name}.jpeg")
try:
async with self.session.get(link, raise_for_status=True) as response:
with open(image_path, "wb") as output_file:
async for chunk in response.content.iter_chunked(8192):
output_file.write(chunk)
image_path_list.append(image_path)
except aiohttp.client_exceptions.InvalidURL as url_exception:
raise Exception("Inappropriate contents found in the generated images. Please try again or try another prompt.") from url_exception # noqa: E501
else:
image_name = str(uuid4())
if links:
link = links.pop()
try:
async with self.session.get(link, raise_for_status=True) as response:
image_path = os.path.join(output_dir, f"{image_name}.jpeg")
with open(image_path, "wb") as output_file:
async for chunk in response.content.iter_chunked(8192):
output_file.write(chunk)
image_path_list.append(image_path)
except aiohttp.client_exceptions.InvalidURL as url_exception:
raise Exception("Inappropriate contents found in the generated images. Please try again or try another prompt.") from url_exception # noqa: E501
return image_path_list

View file

@ -1,37 +0,0 @@
# Changelog
## 1.5.3
- Make gptbot more compatible by using non-streaming method
## 1.5.2
- Expose more stable diffusion webui api parameters
## 1.5.1
- fix: set timeout not work in image generation
## 1.5.0
- Fix localai v2.0+ image generation
- Fallback to gpt-3.5-turbo when caculate tokens using custom model
## 1.4.1
- Fix variable type imported from environment variable
- Bump pre-commit hook version
## 1.4.0
- Fix access_token login method not work in E2EE Room
## 1.3.0
- remove support for bing,bard,pandora
- refactor chat logic, add self host model support
- support new image generation endpoint
- admin system to manage langchain(flowise backend)
## 1.2.0
- rename `api_key` to `openai_api_key` in `config.json`
- rename `bing_api_endpoint` to `api_endpoint` in `config.json` and `env` file
- add `temperature` option to control ChatGPT model temperature
- remove `jailbreakEnabled` option
- session isolation for `!chat`, `!bing`, `!bard` command
- `!new + {chat,bing,bard,talk}` now can be used to create new conversation
- send some error message to user
- bug fix and code cleanup

View file

@ -2,15 +2,18 @@ FROM python:3.11-alpine as base
FROM base as pybuilder FROM base as pybuilder
# RUN sed -i 's|v3\.\d*|edge|' /etc/apk/repositories # RUN sed -i 's|v3\.\d*|edge|' /etc/apk/repositories
RUN apk update && apk add --no-cache olm-dev gcc musl-dev libmagic libffi-dev cmake make g++ git python3-dev RUN apk update && apk add --no-cache olm-dev gcc musl-dev libmagic libffi-dev
COPY requirements.txt /requirements.txt COPY requirements.txt /requirements.txt
RUN pip install -U pip setuptools wheel && pip install --user -r /requirements.txt && rm /requirements.txt RUN pip install -U pip setuptools wheel && pip install --user -r /requirements.txt && rm /requirements.txt
FROM base as runner FROM base as runner
RUN apk update && apk add --no-cache olm-dev libmagic libffi-dev RUN apk update && apk add --no-cache olm-dev libmagic libffi-dev
COPY --from=pybuilder /root/.local /usr/local COPY --from=pybuilder /root/.local /usr/local
COPY . /app COPY . /app
FROM runner FROM runner
WORKDIR /app WORKDIR /app
CMD ["python", "src/main.py"] CMD ["python", "main.py"]

View file

@ -1,29 +1,29 @@
## Introduction ## Introduction
This is a simple Matrix bot that support using OpenAI API, Langchain to generate responses from user inputs. The bot responds to these commands: `!gpt`, `!chat`, `!pic`, `!new`, `!lc` and `!help` 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 six types of prompts: `!gpt`, `!chat` and `!bing` and `!pic` and `!bard` and `!lc` depending on the first word of the prompt.
![Bing](https://user-images.githubusercontent.com/32976627/231073146-3e380217-a6a2-413d-9203-ab36965b909d.png)
![image](https://user-images.githubusercontent.com/32976627/232036790-e830145c-914e-40be-b3e6-c02cba93329c.png)
![ChatGPT](https://i.imgur.com/kK4rnPf.jpeg) ![ChatGPT](https://i.imgur.com/kK4rnPf.jpeg)
## Feature ## Feature
1. Support official openai api and self host models([LocalAI](https://localai.io/model-compatibility/)) 1. Support Openai ChatGPT and Bing AI and Google Bard and Langchain([Flowise](https://github.com/FlowiseAI/Flowise))
2. Support E2E Encrypted Room 2. Support Bing Image Creator
3. Colorful code blocks 3. Support E2E Encrypted Room
4. Langchain([Flowise](https://github.com/FlowiseAI/Flowise)) 4. Colorful code blocks
5. Image Generation with [DALL·E](https://platform.openai.com/docs/api-reference/images/create) or [LocalAI](https://localai.io/features/image-generation/) or [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/API)
## Installation and Setup ## Installation and Setup
Docker method(Recommended):<br> Docker method(Recommended):<br>
Edit `config.json` or `.env` with proper values <br> Edit `config.json` or `.env` with proper values <br>
For explainations and complete parameter list see: https://github.com/hibobmaster/matrix_chatgpt_bot/wiki <br> For explainations and complete parameter list see: https://github.com/hibobmaster/matrix_chatgpt_bot/wiki <br>
Create two empty file, for persist database only<br> Create an empty file, for persist database only<br>
```bash ```bash
touch sync_db manage_db touch db
sudo docker compose up -d sudo docker compose up -d
``` ```
manage_db(can be ignored) is for langchain agent, sync_db is for matrix sync database<br>
<hr> <hr>
Normal Method:<br> Normal Method:<br>
system dependece: <code>libolm-dev</code> system dependece: <code>libolm-dev</code>
@ -44,9 +44,13 @@ pip install -U pip setuptools wheel
pip install -r requirements.txt pip install -r requirements.txt
``` ```
3. Create a new config.json file and complete it with the necessary information:<br> 3. Create a new config.json file and fill it with the necessary information:<br>
Use password to login(recommended) or provide `access_token` <br>
If not set:<br> If not set:<br>
`room_id`: bot will work in the room where it is in <br> `room_id`: bot will work in the room where it is in <br>
`api_key`: `!chat` command will not work <br>
`bing_api_endpoint`: `!bing` command will not work <br>
`bing_auth_cookie`: `!pic` command will not work
```json ```json
{ {
@ -55,20 +59,22 @@ pip install -r requirements.txt
"password": "YOUR_PASSWORD", "password": "YOUR_PASSWORD",
"device_id": "YOUR_DEVICE_ID", "device_id": "YOUR_DEVICE_ID",
"room_id": "YOUR_ROOM_ID", "room_id": "YOUR_ROOM_ID",
"openai_api_key": "YOUR_API_KEY", "api_key": "YOUR_API_KEY",
"gpt_api_endpoint": "xxxxxxxxx" "access_token": "xxxxxxxxxxxxxx",
"bing_api_endpoint": "xxxxxxxxx",
"bing_auth_cookie": "xxxxxxxxxx"
} }
``` ```
4. Launch the bot: 4. Start the bot:
``` ```
python src/main.py python main.py
``` ```
## Usage ## Usage
To interact with the bot, simply send a message to the bot in the Matrix room with one of the following prompts:<br> To interact with the bot, simply send a message to the bot in the Matrix room with one of the two prompts:<br>
- `!help` help message - `!help` help message
- `!gpt` To generate a one time response: - `!gpt` To generate a one time response:
@ -83,34 +89,38 @@ To interact with the bot, simply send a message to the bot in the Matrix room wi
!chat Can you tell me a joke? !chat Can you tell me a joke?
``` ```
- `!bing` To chat with Bing AI with context conversation
```
!bing Do you know Victor Marie Hugo?
```
- `!bard` To chat with Google's Bard
```
!bard Building a website can be done in 10 simple steps
```
- `!lc` To chat using langchain api endpoint - `!lc` To chat using langchain api endpoint
``` ```
!lc All the world is a stage !lc 人生如音乐,欢乐且自由
``` ```
- `!pic` To generate an image using openai DALL·E or LocalAI - `!pic` To generate an image from Microsoft Bing
``` ```
!pic A bridal bouquet made of succulents !pic A bridal bouquet made of succulents
``` ```
- `!agent` display or set langchain agent
```
!agent list
!agent use {agent_name}
```
- `!new + {chat}` Start a new converstaion
LangChain(flowise) admin: https://github.com/hibobmaster/matrix_chatgpt_bot/wiki/Langchain-(flowise) ## Bing AI and Image Generation
## Image Generation
![demo1](https://i.imgur.com/voeomsF.jpg)
![demo2](https://i.imgur.com/BKZktWd.jpg)
https://github.com/hibobmaster/matrix_chatgpt_bot/wiki/ <br> https://github.com/hibobmaster/matrix_chatgpt_bot/wiki/ <br>
![](https://i.imgur.com/KuYddd5.jpg)
![](https://i.imgur.com/3SRQdN0.jpg)
## Thanks ## Thanks
1. [matrix-nio](https://github.com/poljar/matrix-nio) 1. [matrix-nio](https://github.com/poljar/matrix-nio)
2. [acheong08](https://github.com/acheong08) 2. [acheong08](https://github.com/acheong08)
3. [8go](https://github.com/8go/) 3. [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api)
4. [8go](https://github.com/8go/)
<a href="https://jb.gg/OpenSourceSupport" target="_blank"> <a href="https://jb.gg/OpenSourceSupport" target="_blank">
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main) logo." width="200" height="200"> <img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main) logo." width="200" height="200">

39
askgpt.py Normal file
View file

@ -0,0 +1,39 @@
import aiohttp
import asyncio
import json
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) -> str:
jsons = {
"model": "gpt-3.5-turbo",
"messages": [
{
"role": "user",
"content": prompt,
},
],
}
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
# wait 2s
await asyncio.sleep(2)
continue
resp = await response.read()
return json.loads(resp)['choices'][0]['message']['content']
except Exception as e:
raise Exception(e)

103
bard.py Normal file
View file

@ -0,0 +1,103 @@
"""
Code derived from: https://github.com/acheong08/Bard/blob/main/src/Bard.py
"""
import random
import string
import re
import json
import requests
class Bardbot:
"""
A class to interact with Google Bard.
Parameters
session_id: str
The __Secure-1PSID cookie.
"""
__slots__ = [
"headers",
"_reqid",
"SNlM0e",
"conversation_id",
"response_id",
"choice_id",
"session",
]
def __init__(self, session_id):
headers = {
"Host": "bard.google.com",
"X-Same-Domain": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"Origin": "https://bard.google.com",
"Referer": "https://bard.google.com/",
}
self._reqid = int("".join(random.choices(string.digits, k=4)))
self.conversation_id = ""
self.response_id = ""
self.choice_id = ""
self.session = requests.Session()
self.session.headers = headers
self.session.cookies.set("__Secure-1PSID", session_id)
self.SNlM0e = self.__get_snlm0e()
def __get_snlm0e(self):
resp = self.session.get(url="https://bard.google.com/", timeout=10)
# Find "SNlM0e":"<ID>"
if resp.status_code != 200:
raise Exception("Could not get Google Bard")
SNlM0e = re.search(r"SNlM0e\":\"(.*?)\"", resp.text).group(1)
return SNlM0e
def ask(self, message: str) -> dict:
"""
Send a message to Google Bard and return the response.
:param message: The message to send to Google Bard.
:return: A dict containing the response from Google Bard.
"""
# url params
params = {
"bl": "boq_assistant-bard-web-server_20230326.21_p0",
"_reqid": str(self._reqid),
"rt": "c",
}
# message arr -> data["f.req"]. Message is double json stringified
message_struct = [
[message],
None,
[self.conversation_id, self.response_id, self.choice_id],
]
data = {
"f.req": json.dumps([None, json.dumps(message_struct)]),
"at": self.SNlM0e,
}
# do the request!
resp = self.session.post(
"https://bard.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate",
params=params,
data=data,
timeout=120,
)
chat_data = json.loads(resp.content.splitlines()[3])[0][2]
if not chat_data:
return {"content": f"Google Bard encountered an error: {resp.content}."}
json_chat_data = json.loads(chat_data)
results = {
"content": json_chat_data[0][0],
"conversation_id": json_chat_data[1][0],
"response_id": json_chat_data[1][1],
"factualityQueries": json_chat_data[3],
"textQuery": json_chat_data[2][0] if json_chat_data[2] is not None else "",
"choices": [{"id": i[0], "content": i[1]} for i in json_chat_data[4]],
}
self.conversation_id = results["conversation_id"]
self.response_id = results["response_id"]
self.choice_id = results["choices"][0]["id"]
self._reqid += 100000
return results

52
bing.py Normal file
View file

@ -0,0 +1,52 @@
import aiohttp
import json
import asyncio
from log import getlogger
# api_endpoint = "http://localhost:3000/conversation"
logger = getlogger()
class BingBot:
def __init__(self, session: aiohttp.ClientSession, bing_api_endpoint: str, jailbreakEnabled: bool = True):
self.data = {
'clientOptions.clientToUse': 'bing',
}
self.bing_api_endpoint = bing_api_endpoint
self.session = session
self.jailbreakEnabled = jailbreakEnabled
if self.jailbreakEnabled:
self.data['jailbreakConversationId'] = True
async def ask_bing(self, prompt) -> str:
self.data['message'] = prompt
max_try = 2
while max_try > 0:
try:
resp = await self.session.post(url=self.bing_api_endpoint, json=self.data, timeout=120)
status_code = resp.status
body = await resp.read()
if not status_code == 200:
# print failed reason
logger.warning(str(resp.reason))
max_try = max_try - 1
# print(await resp.text())
await asyncio.sleep(2)
continue
json_body = json.loads(body)
if self.jailbreakEnabled:
self.data['jailbreakConversationId'] = json_body['jailbreakConversationId']
self.data['parentMessageId'] = json_body['messageId']
else:
self.data['conversationSignature'] = json_body['conversationSignature']
self.data['conversationId'] = json_body['conversationId']
self.data['clientId'] = json_body['clientId']
self.data['invocationId'] = json_body['invocationId']
return json_body['details']['adaptiveCards'][0]['body'][0]['text']
except Exception as e:
logger.error("Error Exception", exc_info=True)
return "Error, please retry"

717
bot.py Normal file
View file

@ -0,0 +1,717 @@
import asyncio
import os
import re
import sys
import traceback
from typing import Union, Optional
import aiohttp
from nio import (AsyncClient, AsyncClientConfig, InviteMemberEvent, JoinError,
KeyVerificationCancel, KeyVerificationEvent, EncryptionError,
KeyVerificationKey, KeyVerificationMac, KeyVerificationStart,
LocalProtocolError, LoginResponse, MatrixRoom, MegolmEvent,
RoomMessageText, ToDeviceError)
from nio.store.database import SqliteStore
from askgpt import askGPT
from bing import BingBot
from BingImageGen import ImageGenAsync
from log import getlogger
from send_image import send_room_image
from send_message import send_room_message
from v3 import Chatbot
from bard import Bardbot
from flowise import flowise_query
logger = getlogger()
class Bot:
def __init__(
self,
homeserver: str,
user_id: str,
device_id: str,
chatgpt_api_endpoint: str = os.environ.get(
"CHATGPT_API_ENDPOINT") or "https://api.openai.com/v1/chat/completions",
api_key: Union[str, None] = None,
room_id: Union[str, None] = None,
bing_api_endpoint: 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,
import_keys_path: Optional[str] = None,
import_keys_password: Optional[str] = None,
flowise_api_url: Optional[str] = None,
flowise_api_key: Optional[str] = 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")
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.api_key = api_key
self.chatgpt_api_endpoint = chatgpt_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.session = aiohttp.ClientSession()
if bing_api_endpoint is None:
self.bing_api_endpoint = ''
else:
self.bing_api_endpoint = bing_api_endpoint
if jailbreakEnabled is None:
self.jailbreakEnabled = True
else:
self.jailbreakEnabled = jailbreakEnabled
if bing_auth_cookie is None:
self.bing_auth_cookie = ''
else:
self.bing_auth_cookie = bing_auth_cookie
if markdown_formatted is None:
self.markdown_formatted = False
else:
self.markdown_formatted = markdown_formatted
if output_four_images is None:
self.output_four_images = False
else:
self.output_four_images = output_four_images
# initialize AsyncClient object
self.store_path = os.getcwd()
self.config = AsyncClientConfig(store=SqliteStore,
store_name="db",
store_sync_tokens=True,
encryption_enabled=True,
)
self.client = AsyncClient(homeserver=self.homeserver, user=self.user_id,
device_id=self.device_id,
config=self.config, store_path=self.store_path,)
if self.access_token is not None:
self.client.access_token = self.access_token
# setup event callbacks
self.client.add_event_callback(
self.message_callback, (RoomMessageText, ))
self.client.add_event_callback(
self.decryption_failure, (MegolmEvent, ))
self.client.add_event_callback(
self.invite_callback, (InviteMemberEvent, ))
self.client.add_to_device_callback(
self.to_device_callback, (KeyVerificationEvent, ))
# 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*.*$")
# initialize chatbot and chatgpt_api_endpoint
if self.api_key is not None:
self.chatbot = Chatbot(api_key=self.api_key, timeout=120)
self.chatgpt_api_endpoint = self.chatgpt_api_endpoint
# request header for !gpt command
self.headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
}
# initialize askGPT class
self.askgpt = askGPT(self.session)
# initialize bingbot
if self.bing_api_endpoint != '':
self.bingbot = BingBot(
self.session, bing_api_endpoint, jailbreakEnabled=self.jailbreakEnabled)
# initialize BingImageGenAsync
if self.bing_auth_cookie != '':
self.imageGen = ImageGenAsync(self.bing_auth_cookie, quiet=True)
# initialize Bardbot
if bard_token is not None:
self.bardbot = Bardbot(self.bard_token)
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()
# message_callback RoomMessageText event
async def message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None:
if self.room_id is None:
room_id = room.room_id
else:
# if event room id does not match the room id in config, return
if room.room_id != self.room_id:
return
room_id = self.room_id
# reply event_id
reply_to_event_id = event.event_id
# sender_id
sender_id = event.sender
# user_message
raw_user_message = event.body
# print info to console
logger.info(
f"Message received in room {room.display_name}\n"
f"{room.user_name(event.sender)} | {raw_user_message}"
)
# prevent command trigger loop
if self.user_id != event.sender:
# remove newline character from event.body
content_body = re.sub("\r\n|\r|\n", " ", raw_user_message)
# chatgpt
n = self.chat_prog.match(content_body)
if n:
prompt = n.group(1)
if self.api_key is not None:
try:
asyncio.create_task(self.chat(room_id,
reply_to_event_id,
prompt,
sender_id,
raw_user_message
)
)
except Exception as e:
logger.error(e, exc_info=True)
await send_room_message(self.client, room_id,
reply_message=str(e))
else:
logger.warning("No API_KEY provided")
await send_room_message(self.client, room_id,
reply_message="API_KEY not provided")
m = self.gpt_prog.match(content_body)
if m:
prompt = m.group(1)
try:
asyncio.create_task(self.gpt(
room_id,
reply_to_event_id,
prompt, sender_id,
raw_user_message
)
)
except Exception as e:
logger.error(e, exc_info=True)
await send_room_message(self.client, room_id, reply_message=str(e))
# bing ai
if self.bing_api_endpoint != '':
b = self.bing_prog.match(content_body)
if b:
prompt = b.group(1)
# raw_content_body used for construct formatted_body
try:
asyncio.create_task(self.bing(
room_id,
reply_to_event_id,
prompt,
sender_id,
raw_user_message
)
)
except Exception as e:
logger.error(e, exc_info=True)
await send_room_message(self.client, room_id,
reply_message=str(e))
# 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)
await send_room_message(self.client, room_id,
reply_message=str(e))
# Google's Bard
if self.bard_token is not None:
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)
await send_room_message(self.client, room_id, reply_message={e})
# lc command
if self.flowise_api_url is not None:
m = self.lc_prog.match(content_body)
if m:
prompt = m.group(1)
try:
asyncio.create_task(self.lc(
room_id,
reply_to_event_id,
prompt,
sender_id,
raw_user_message
)
)
except Exception as e:
logger.error(e, exc_info=True)
await send_room_message(self.client, room_id, reply_message={e})
# help command
h = self.help_prog.match(content_body)
if h:
asyncio.create_task(self.help(room_id))
# message_callback decryption_failure event
async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent) -> None:
if not isinstance(event, MegolmEvent):
return
logger.error(
f"Failed to decrypt message: {event.event_id} \
from {event.sender} in {room.room_id}\n" +
"Please make sure the bot current session is verified"
)
# invite_callback event
async def invite_callback(self, room: MatrixRoom, event: InviteMemberEvent) -> None:
"""Handle an incoming invite event.
If an invite is received, then join the room specified in the invite.
code copied from: https://github.com/8go/matrix-eno-bot/blob/ad037e02bd2960941109e9526c1033dd157bb212/callbacks.py#L104
"""
logger.debug(f"Got invite to {room.room_id} from {event.sender}.")
# Attempt to join 3 times before giving up
for attempt in range(3):
result = await self.client.join(room.room_id)
if type(result) == JoinError:
logger.error(
f"Error joining room {room.room_id} (attempt %d): %s",
attempt, result.message,
)
else:
break
else:
logger.error("Unable to join room: %s", room.room_id)
# Successfully joined room
logger.info(f"Joined {room.room_id}")
# to_device_callback event
async def to_device_callback(self, event: KeyVerificationEvent) -> None:
"""Handle events sent to device.
Specifically this will perform Emoji verification.
It will accept an incoming Emoji verification requests
and follow the verification protocol.
code copied from: https://github.com/8go/matrix-eno-bot/blob/ad037e02bd2960941109e9526c1033dd157bb212/callbacks.py#L127
"""
try:
client = self.client
logger.debug(
f"Device Event of type {type(event)} received in "
"to_device_cb().")
if isinstance(event, KeyVerificationStart): # first step
""" first step: receive KeyVerificationStart
KeyVerificationStart(
source={'content':
{'method': 'm.sas.v1',
'from_device': 'DEVICEIDXY',
'key_agreement_protocols':
['curve25519-hkdf-sha256', 'curve25519'],
'hashes': ['sha256'],
'message_authentication_codes':
['hkdf-hmac-sha256', 'hmac-sha256'],
'short_authentication_string':
['decimal', 'emoji'],
'transaction_id': 'SomeTxId'
},
'type': 'm.key.verification.start',
'sender': '@user2:example.org'
},
sender='@user2:example.org',
transaction_id='SomeTxId',
from_device='DEVICEIDXY',
method='m.sas.v1',
key_agreement_protocols=[
'curve25519-hkdf-sha256', 'curve25519'],
hashes=['sha256'],
message_authentication_codes=[
'hkdf-hmac-sha256', 'hmac-sha256'],
short_authentication_string=['decimal', 'emoji'])
"""
if "emoji" not in event.short_authentication_string:
estr = ("Other device does not support emoji verification "
f"{event.short_authentication_string}. Aborting.")
logger.info(estr)
return
resp = await client.accept_key_verification(
event.transaction_id)
if isinstance(resp, ToDeviceError):
estr = f"accept_key_verification() failed with {resp}"
logger.info(estr)
sas = client.key_verifications[event.transaction_id]
todevice_msg = sas.share_key()
resp = await client.to_device(todevice_msg)
if isinstance(resp, ToDeviceError):
estr = f"to_device() failed with {resp}"
logger.info(estr)
elif isinstance(event, KeyVerificationCancel): # anytime
""" at any time: receive KeyVerificationCancel
KeyVerificationCancel(source={
'content': {'code': 'm.mismatched_sas',
'reason': 'Mismatched authentication string',
'transaction_id': 'SomeTxId'},
'type': 'm.key.verification.cancel',
'sender': '@user2:example.org'},
sender='@user2:example.org',
transaction_id='SomeTxId',
code='m.mismatched_sas',
reason='Mismatched short authentication string')
"""
# There is no need to issue a
# client.cancel_key_verification(tx_id, reject=False)
# here. The SAS flow is already cancelled.
# We only need to inform the user.
estr = (f"Verification has been cancelled by {event.sender} "
f"for reason \"{event.reason}\".")
logger.info(estr)
elif isinstance(event, KeyVerificationKey): # second step
""" Second step is to receive KeyVerificationKey
KeyVerificationKey(
source={'content': {
'key': 'SomeCryptoKey',
'transaction_id': 'SomeTxId'},
'type': 'm.key.verification.key',
'sender': '@user2:example.org'
},
sender='@user2:example.org',
transaction_id='SomeTxId',
key='SomeCryptoKey')
"""
sas = client.key_verifications[event.transaction_id]
logger.info(f"{sas.get_emoji()}")
# don't log the emojis
# The bot process must run in forground with a screen and
# keyboard so that user can accept/reject via keyboard.
# For emoji verification bot must not run as service or
# in background.
# yn = input("Do the emojis match? (Y/N) (C for Cancel) ")
# automatic match, so we use y
yn = "y"
if yn.lower() == "y":
estr = ("Match! The verification for this "
"device will be accepted.")
logger.info(estr)
resp = await client.confirm_short_auth_string(
event.transaction_id)
if isinstance(resp, ToDeviceError):
estr = ("confirm_short_auth_string() "
f"failed with {resp}")
logger.info(estr)
elif yn.lower() == "n": # no, don't match, reject
estr = ("No match! Device will NOT be verified "
"by rejecting verification.")
logger.info(estr)
resp = await client.cancel_key_verification(
event.transaction_id, reject=True)
if isinstance(resp, ToDeviceError):
estr = (f"cancel_key_verification failed with {resp}")
logger.info(estr)
else: # C or anything for cancel
estr = ("Cancelled by user! Verification will be "
"cancelled.")
logger.info(estr)
resp = await client.cancel_key_verification(
event.transaction_id, reject=False)
if isinstance(resp, ToDeviceError):
estr = (f"cancel_key_verification failed with {resp}")
logger.info(estr)
elif isinstance(event, KeyVerificationMac): # third step
""" Third step is to receive KeyVerificationMac
KeyVerificationMac(
source={'content': {
'mac': {'ed25519:DEVICEIDXY': 'SomeKey1',
'ed25519:SomeKey2': 'SomeKey3'},
'keys': 'SomeCryptoKey4',
'transaction_id': 'SomeTxId'},
'type': 'm.key.verification.mac',
'sender': '@user2:example.org'},
sender='@user2:example.org',
transaction_id='SomeTxId',
mac={'ed25519:DEVICEIDXY': 'SomeKey1',
'ed25519:SomeKey2': 'SomeKey3'},
keys='SomeCryptoKey4')
"""
sas = client.key_verifications[event.transaction_id]
try:
todevice_msg = sas.get_mac()
except LocalProtocolError as e:
# e.g. it might have been cancelled by ourselves
estr = (f"Cancelled or protocol error: Reason: {e}.\n"
f"Verification with {event.sender} not concluded. "
"Try again?")
logger.info(estr)
else:
resp = await client.to_device(todevice_msg)
if isinstance(resp, ToDeviceError):
estr = f"to_device failed with {resp}"
logger.info(estr)
estr = (f"sas.we_started_it = {sas.we_started_it}\n"
f"sas.sas_accepted = {sas.sas_accepted}\n"
f"sas.canceled = {sas.canceled}\n"
f"sas.timed_out = {sas.timed_out}\n"
f"sas.verified = {sas.verified}\n"
f"sas.verified_devices = {sas.verified_devices}\n")
logger.info(estr)
estr = ("Emoji verification was successful!\n"
"Initiate another Emoji verification from "
"another device or room if desired. "
"Or if done verifying, hit Control-C to stop the "
"bot in order to restart it as a service or to "
"run it in the background.")
logger.info(estr)
else:
estr = (f"Received unexpected event type {type(event)}. "
f"Event is {event}. Event will be ignored.")
logger.info(estr)
except BaseException:
estr = traceback.format_exc()
logger.info(estr)
# !chat command
async def chat(self, room_id, reply_to_event_id, prompt, sender_id,
raw_user_message):
await self.client.room_typing(room_id, timeout=120000)
try:
text = await self.chatbot.ask_async(prompt)
except Exception as e:
raise Exception(e)
try:
text = text.strip()
await send_room_message(self.client, room_id, reply_message=text,
reply_to_event_id="", sender_id=sender_id,
user_message=raw_user_message,
markdown_formatted=self.markdown_formatted)
except Exception as e:
logger.error(f"Error: {e}", exc_info=True)
# !gpt command
async def gpt(self, room_id, reply_to_event_id, prompt, sender_id,
raw_user_message) -> None:
try:
# sending typing state
await self.client.room_typing(room_id, timeout=240000)
# timeout 240s
text = await asyncio.wait_for(self.askgpt.oneTimeAsk(prompt,
self.chatgpt_api_endpoint,
self.headers),
timeout=240)
except TimeoutError:
logger.error("TimeoutException", exc_info=True)
raise Exception("Timeout error")
except Exception as e:
raise Exception(e)
try:
text = text.strip()
await send_room_message(self.client, room_id, reply_message=text,
reply_to_event_id="", sender_id=sender_id,
user_message=raw_user_message,
markdown_formatted=self.markdown_formatted)
except Exception as e:
logger.error(f"Error: {e}", exc_info=True)
# !bing command
async def bing(self, room_id, reply_to_event_id, prompt, sender_id,
raw_user_message) -> None:
try:
# sending typing state
await self.client.room_typing(room_id, timeout=180000)
# timeout 240s
text = await asyncio.wait_for(self.bingbot.ask_bing(prompt), timeout=240)
except TimeoutError:
logger.error("timeoutException", exc_info=True)
raise Exception("Timeout error")
except Exception as e:
raise Exception(e)
try:
text = text.strip()
await send_room_message(self.client, room_id, reply_message=text,
reply_to_event_id="", sender_id=sender_id,
user_message=raw_user_message,
markdown_formatted=self.markdown_formatted)
except Exception as e:
logger.error(e, exc_info=True)
# !bard command
async def bard(self, room_id, reply_to_event_id, prompt, sender_id,
raw_user_message) -> None:
try:
# sending typing state
await self.client.room_typing(room_id)
response = await asyncio.to_thread(self.bardbot.ask, prompt)
except Exception as e:
raise Exception(e)
try:
content = str(response['content']).strip()
await send_room_message(self.client, room_id, reply_message=content,
reply_to_event_id="", sender_id=sender_id,
user_message=raw_user_message,
markdown_formatted=self.markdown_formatted)
except Exception as e:
logger.error(e, exc_info=True)
# !lc command
async def lc(self, room_id, reply_to_event_id, prompt, sender_id,
raw_user_message) -> None:
try:
# sending typing state
await self.client.room_typing(room_id)
if self.flowise_api_key is not None:
headers = {'Authorization': f'Bearer {self.flowise_api_key}'}
response = await asyncio.to_thread(flowise_query,
self.flowise_api_url, prompt, headers)
else:
response = await asyncio.to_thread(flowise_query,
self.flowise_api_url, prompt)
await send_room_message(self.client, room_id, reply_message=response,
reply_to_event_id="", sender_id=sender_id,
user_message=raw_user_message,
markdown_formatted=self.markdown_formatted)
except Exception as e:
raise Exception(e)
# !pic command
async def pic(self, room_id, prompt):
try:
await self.client.room_typing(room_id, timeout=180000)
# generate image
try:
links = await self.imageGen.get_images(prompt)
image_path_list = await self.imageGen.save_images(links, "images",
self.output_four_images)
except Exception as e:
logger.error(f"Image Generation error: {e}", exc_info=True)
raise Exception(e)
# send image
try:
for image_path in image_path_list:
await send_room_image(self.client, room_id, image_path)
await self.client.room_typing(room_id, typing_state=False)
except Exception as e:
logger.error(e, exc_info=True)
except Exception as e:
logger.error(e, exc_info=True)
# !help command
async def help(self, room_id):
try:
# sending typing state
await self.client.room_typing(room_id)
help_info = "!gpt [prompt], generate response without context conversation\n" + \
"!chat [prompt], chat with context conversation\n" + \
"!bing [prompt], chat with context conversation powered by Bing AI\n" + \
"!bard [prompt], chat with Google's Bard\n" + \
"!pic [prompt], Image generation by Microsoft Bing\n" + \
"!lc [prompt], chat using langchain api\n" + \
"!help, help message" # noqa: E501
await send_room_message(self.client, room_id, reply_message=help_info)
except Exception as e:
logger.error(e, exc_info=True)
# 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)
# import keys
async def import_keys(self):
resp = await self.client.import_keys(
self.import_keys_path,
self.import_keys_password
)
if isinstance(resp, EncryptionError):
logger.error(f"import_keys failed with {resp}")
else:
logger.info(
"import_keys success, please remove import_keys configuration!!!")
# sync messages in the room
async def sync_forever(self, timeout=30000, full_state=True) -> None:
await self.client.sync_forever(timeout=timeout, full_state=full_state)

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: unless-stopped restart: always
# build: # build:
# context: . # context: .
# dockerfile: ./Dockerfile # dockerfile: ./Dockerfile
@ -11,14 +11,21 @@ services:
volumes: volumes:
# use env file or config.json # use env file or config.json
# - ./config.json:/app/config.json # - ./config.json:/app/config.json
# use touch to create empty db file, for persist database only # use touch to create an empty file db, for persist database only
# manage_db(can be ignored) is for langchain agent, sync_db is for matrix sync database - ./db:/app/db
- ./sync_db:/app/sync_db
# - ./manage_db:/app/manage_db
# import_keys path # import_keys path
# - ./element-keys.txt:/app/element-keys.txt # - ./element-keys.txt:/app/element-keys.txt
networks: networks:
- matrix_network - matrix_network
# api:
# # bing api
# image: ghcr.io/waylaidwanderer/node-chatgpt-api:latest
# container_name: node-chatgpt-api
# restart: always
# volumes:
# - ./settings.js:/var/chatgpt-api/settings.js
# networks:
# - matrix_network
networks: networks:
matrix_network: matrix_network:

View file

@ -1,7 +0,0 @@
{
"homeserver": "https://matrix-client.matrix.org",
"user_id": "@lullap:xxxxx.org",
"password": "xxxxxxxxxxxxxxxxxx",
"device_id": "MatrixChatGPTBot",
"openai_api_key": "xxxxxxxxxxxxxxxxxxxxxxxx"
}

19
config.json.sample Normal file
View file

@ -0,0 +1,19 @@
{
"homeserver": "https://matrix.qqs.tw",
"user_id": "@lullap:xxxxx.org",
"password": "xxxxxxxxxxxxxxxxxx",
"device_id": "ECYEOKVPLG",
"room_id": "!FYCmBSkCRUNvZDBaDQ:matrix.qqs.tw",
"api_key": "xxxxxxxxxxxxxxxxxxxxxxxx",
"bing_api_endpoint": "http://api:3000/conversation",
"jailbreakEnabled": true,
"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="
}

19
flowise.py Normal file
View file

@ -0,0 +1,19 @@
import requests
def flowise_query(api_url: str, prompt: str, headers: dict = None) -> str:
"""
Sends a query to the Flowise API and returns the response.
Args:
api_url (str): The URL of the Flowise API.
prompt (str): The question to ask the API.
Returns:
str: The response from the API.
"""
if headers:
response = requests.post(api_url, json={"question": prompt},
headers=headers, timeout=120)
else:
response = requests.post(api_url, json={"question": prompt}, timeout=120)
return response.text

View file

@ -1,29 +0,0 @@
{
"homeserver": "https://matrix-client.matrix.org",
"user_id": "@lullap:xxxxx.org",
"password": "xxxxxxxxxxxxxxxxxx",
"access_token": "xxxxxxxxxxxxxx",
"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",
"lc_admin": ["@admin:xxxxx.org"],
"image_generation_endpoint": "http://localai:8080/v1/images/generations",
"image_generation_backend": "localai",
"image_generation_size": "512x512",
"sdwui_steps": 20,
"sdwui_sampler_name": "Euler a",
"sdwui_cfg_scale": 7,
"image_format": "webp",
"timeout": 120.0
}

View file

@ -1,8 +1,4 @@
import logging import logging
import os
from pathlib import Path
log_path = Path(os.path.dirname(__file__)).parent / "bot.log"
def getlogger(): def getlogger():
@ -13,19 +9,18 @@ def getlogger():
# create handlers # create handlers
warn_handler = logging.StreamHandler() warn_handler = logging.StreamHandler()
info_handler = logging.StreamHandler() info_handler = logging.StreamHandler()
error_handler = logging.FileHandler("bot.log", mode="a") error_handler = logging.FileHandler('bot.log', mode='a')
warn_handler.setLevel(logging.WARNING) warn_handler.setLevel(logging.WARNING)
error_handler.setLevel(logging.ERROR) error_handler.setLevel(logging.ERROR)
info_handler.setLevel(logging.INFO) info_handler.setLevel(logging.INFO)
# create formatters # create formatters
warn_format = logging.Formatter( warn_format = logging.Formatter(
"%(asctime)s - %(funcName)s - %(levelname)s - %(message)s", '%(asctime)s - %(funcName)s - %(levelname)s - %(message)s')
)
error_format = logging.Formatter( error_format = logging.Formatter(
"%(asctime)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s", '%(asctime)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s')
) info_format = logging.Formatter(
info_format = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") '%(asctime)s - %(levelname)s - %(message)s')
# set formatter # set formatter
warn_handler.setFormatter(warn_format) warn_handler.setFormatter(warn_format)

78
main.py Normal file
View file

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

View file

@ -1,9 +0,0 @@
aiofiles
httpx
Markdown
matrix-nio[e2e]
Pillow
tiktoken
tenacity
python-magic
pytest

View file

@ -1,8 +1,51 @@
aiofiles aiofiles==23.1.0
httpx aiohttp==3.8.4
Markdown aiohttp-socks==0.7.1
matrix-nio[e2e] aiosignal==1.3.1
Pillow anyio==3.6.2
tiktoken async-timeout==4.0.2
tenacity atomicwrites==1.4.1
python-magic attrs==22.2.0
blobfile==2.0.1
cachetools==4.2.4
certifi==2022.12.7
cffi==1.15.1
charset-normalizer==3.1.0
cryptography==40.0.1
filelock==3.11.0
frozenlist==1.3.3
future==0.18.3
h11==0.14.0
h2==4.1.0
hpack==4.0.0
httpcore==0.16.3
httpx==0.23.3
hyperframe==6.0.1
idna==3.4
jsonschema==4.17.3
Logbook==1.5.3
lxml==4.9.2
Markdown==3.4.3
matrix-nio[e2e]==0.20.2
multidict==6.0.4
peewee==3.16.0
Pillow==9.5.0
pycparser==2.21
pycryptodome==3.17
pycryptodomex==3.17
pyrsistent==0.19.3
python-cryptography-fernet-wrapper==1.0.4
python-magic==0.4.27
python-olm==3.1.3
python-socks==2.2.0
regex==2023.3.23
requests==2.31.0
rfc3986==1.5.0
six==1.16.0
sniffio==1.3.0
tiktoken==0.3.3
toml==0.10.2
unpaddedbase64==2.1.0
urllib3==1.26.15
wcwidth==0.2.6
yarl==1.8.2

View file

@ -3,18 +3,17 @@ code derived from:
https://matrix-nio.readthedocs.io/en/latest/examples.html#sending-an-image https://matrix-nio.readthedocs.io/en/latest/examples.html#sending-an-image
""" """
import os import os
import aiofiles.os import aiofiles.os
import magic import magic
from log import getlogger
from nio import AsyncClient
from nio import UploadResponse
from PIL import Image from PIL import Image
from nio import AsyncClient, UploadResponse
from log import getlogger
logger = getlogger() logger = getlogger()
async def send_room_image(client: AsyncClient, room_id: str, image: str): async def send_room_image(client: AsyncClient,
room_id: str, image: str):
""" """
image: image path image: image path
""" """
@ -33,14 +32,12 @@ async def send_room_image(client: AsyncClient, room_id: str, image: str):
filesize=file_stat.st_size, filesize=file_stat.st_size,
) )
if not isinstance(resp, UploadResponse): if not isinstance(resp, UploadResponse):
logger.warning(f"Failed to upload image. Failure response: {resp}") logger.warning(f"Failed to generate image. Failure response: {resp}")
await client.room_send( await client.room_send(
room_id, room_id,
message_type="m.room.message", message_type="m.room.message",
content={ content={"msgtype": "m.text",
"msgtype": "m.text", "body": f"Failed to generate image. Failure response: {resp}", },
"body": f"Failed to upload image. Failure response: {resp}",
},
ignore_unverified_devices=True, ignore_unverified_devices=True,
) )
return return
@ -60,5 +57,6 @@ async def send_room_image(client: AsyncClient, room_id: str, image: str):
try: try:
await client.room_send(room_id, message_type="m.room.message", content=content) await client.room_send(room_id, message_type="m.room.message", content=content)
except Exception as e: except Exception as e:
logger.error(f"Image send of file {image} failed.\n Error: {e}", exc_info=True) logger.error(
f"Image send of file {image} failed.\n Error: {e}", exc_info=True)
raise Exception(e) raise Exception(e)

44
send_message.py Normal file
View file

@ -0,0 +1,44 @@
from nio import AsyncClient
import re
import markdown
async def send_room_message(client: AsyncClient,
room_id: str,
reply_message: str,
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
else:
body = r'> <' + sender_id + r'> ' + user_message + r'\n\n' + reply_message
format = r'org.matrix.custom.html'
formatted_body = r'<mx-reply><blockquote><a href="https://matrix.to/#/' + room_id + r'/' + reply_to_event_id \
+ r'">In reply to</a> <a href="https://matrix.to/#/' + sender_id + r'">' + sender_id \
+ r'</a><br>' + user_message + r'</blockquote></mx-reply>' + reply_message
content = {"msgtype": "m.text", "body": body, "format": format, "formatted_body": formatted_body,
"m.relates_to": {"m.in_reply_to": {"event_id": reply_to_event_id}}, }
await client.room_send(
room_id,
message_type="m.room.message",
content=content,
ignore_unverified_devices=True,
)
await client.room_typing(room_id, typing_state=False)

1496
src/bot.py

File diff suppressed because it is too large Load diff

View file

@ -1,41 +0,0 @@
import httpx
async def flowise_query(
api_url: str, prompt: str, session: httpx.AsyncClient, headers: dict = None
) -> str:
"""
Sends a query to the Flowise API and returns the response.
Args:
api_url (str): The URL of the Flowise API.
prompt (str): The question to ask the API.
session (httpx.AsyncClient): The httpx session to use.
headers (dict, optional): The headers to use. Defaults to None.
Returns:
str: The response from the API.
"""
if headers:
response = await session.post(
api_url,
json={"question": prompt},
headers=headers,
)
else:
response = await session.post(api_url, json={"question": prompt})
return response.text
async def test():
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__":
import asyncio
asyncio.run(test())

View file

@ -1,106 +0,0 @@
import httpx
from pathlib import Path
import uuid
import base64
import io
from PIL import Image
async def get_images(
aclient: httpx.AsyncClient,
url: str,
prompt: str,
backend_type: str,
output_path: str,
**kwargs,
) -> list[str]:
timeout = kwargs.get("timeout", 180.0)
if backend_type == "openai":
resp = await aclient.post(
url,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {kwargs.get('api_key')}",
},
json={
"prompt": prompt,
"n": kwargs.get("n", 1),
"size": kwargs.get("size", "512x512"),
"response_format": "b64_json",
},
timeout=timeout,
)
if resp.status_code == 200:
b64_datas = []
for data in resp.json()["data"]:
b64_datas.append(data["b64_json"])
return save_images_b64(b64_datas, output_path, **kwargs)
else:
raise Exception(
f"{resp.status_code} {resp.reason_phrase} {resp.text}",
)
elif backend_type == "sdwui":
resp = await aclient.post(
url,
headers={
"Content-Type": "application/json",
},
json={
"prompt": prompt,
"sampler_name": kwargs.get("sampler_name", "Euler a"),
"cfg_scale": kwargs.get("cfg_scale", 7),
"batch_size": kwargs.get("n", 1),
"steps": kwargs.get("steps", 20),
"width": kwargs.get("width", 512),
"height": kwargs.get("height", 512),
},
timeout=timeout,
)
if resp.status_code == 200:
b64_datas = resp.json()["images"]
return save_images_b64(b64_datas, output_path, **kwargs)
else:
raise Exception(
f"{resp.status_code} {resp.reason_phrase} {resp.text}",
)
elif backend_type == "localai":
resp = await aclient.post(
url,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {kwargs.get('api_key')}",
},
json={
"prompt": prompt,
"size": kwargs.get("size", "512x512"),
},
timeout=timeout,
)
if resp.status_code == 200:
image_url = resp.json()["data"][0]["url"]
return await save_image_url(image_url, aclient, output_path, **kwargs)
def save_images_b64(b64_datas: list[str], path: Path, **kwargs) -> list[str]:
images_path_list = []
for b64_data in b64_datas:
image_path = path / (
str(uuid.uuid4()) + "." + kwargs.get("image_format", "jpeg")
)
img = Image.open(io.BytesIO(base64.decodebytes(bytes(b64_data, "utf-8"))))
img.save(image_path)
images_path_list.append(image_path)
return images_path_list
async def save_image_url(
url: str, aclient: httpx.AsyncClient, path: Path, **kwargs
) -> list[str]:
images_path_list = []
r = await aclient.get(url)
image_path = path / (str(uuid.uuid4()) + "." + kwargs.get("image_format", "jpeg"))
if r.status_code == 200:
img = Image.open(io.BytesIO(r.content))
img.save(image_path)
images_path_list.append(image_path)
return images_path_list

View file

@ -1,200 +0,0 @@
import sqlite3
import sys
from log import getlogger
logger = getlogger()
class LCManager:
def __init__(self):
try:
self.conn = sqlite3.connect("manage_db")
self.c = self.conn.cursor()
self.c.execute(
"""
CREATE TABLE IF NOT EXISTS lc_commands (
command_id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
agent TEXT NOT NULL,
api_url TEXT NOT NULL,
api_key TEXT,
permission INTEGER NOT NULL
)
"""
)
self.conn.commit()
except Exception as e:
logger.error(e, exc_info=True)
sys.exit(1)
def add_command(
self,
username: str,
agent: str,
api_url: str,
api_key: str = None,
permission: int = 0,
) -> None:
# check if username and agent already exists
self.c.execute(
"""
SELECT username, agent FROM lc_commands
WHERE username = ? AND agent = ?
""",
(username, agent),
)
if self.c.fetchone() is not None:
raise Exception("agent already exists")
self.c.execute(
"""
INSERT INTO lc_commands (username, agent, api_url, api_key, permission)
VALUES (?, ?, ?, ?, ?)
""",
(username, agent, api_url, api_key, permission),
)
self.conn.commit()
def get_command_api_url(self, username: str, agent: str) -> list[any]:
self.c.execute(
"""
SELECT api_url FROM lc_commands
WHERE username = ? AND agent = ?
""",
(username, agent),
)
return self.c.fetchall()
def get_command_api_key(self, username: str, agent: str) -> list[any]:
self.c.execute(
"""
SELECT api_key FROM lc_commands
WHERE username = ? AND agent = ?
""",
(username, agent),
)
return self.c.fetchall()
def get_command_permission(self, username: str, agent: str) -> list[any]:
self.c.execute(
"""
SELECT permission FROM lc_commands
WHERE username = ? AND agent = ?
""",
(username, agent),
)
return self.c.fetchall()
def get_command_agent(self, username: str) -> list[any]:
self.c.execute(
"""
SELECT agent FROM lc_commands
WHERE username = ?
""",
(username,),
)
return self.c.fetchall()
def get_specific_by_username(self, username: str) -> list[any]:
self.c.execute(
"""
SELECT * FROM lc_commands
WHERE username = ?
""",
(username,),
)
return self.c.fetchall()
def get_specific_by_agent(self, agent: str) -> list[any]:
self.c.execute(
"""
SELECT * FROM lc_commands
WHERE agent = ?
""",
(agent,),
)
return self.c.fetchall()
def get_all(self) -> list[any]:
self.c.execute(
"""
SELECT * FROM lc_commands
"""
)
return self.c.fetchall()
def update_command_api_url(self, username: str, agent: str, api_url: str) -> None:
self.c.execute(
"""
UPDATE lc_commands
SET api_url = ?
WHERE username = ? AND agent = ?
""",
(api_url, username, agent),
)
self.conn.commit()
def update_command_api_key(self, username: str, agent: str, api_key: str) -> None:
self.c.execute(
"""
UPDATE lc_commands
SET api_key = ?
WHERE username = ? AND agent = ?
""",
(api_key, username, agent),
)
self.conn.commit()
def update_command_permission(
self, username: str, agent: str, permission: int
) -> None:
self.c.execute(
"""
UPDATE lc_commands
SET permission = ?
WHERE username = ? AND agent = ?
""",
(permission, username, agent),
)
self.conn.commit()
def update_command_agent(self, username: str, agent: str, api_url: str) -> None:
# check if agent already exists
self.c.execute(
"""
SELECT agent FROM lc_commands
WHERE agent = ?
""",
(agent,),
)
if self.c.fetchone() is not None:
raise Exception("agent already exists")
self.c.execute(
"""
UPDATE lc_commands
SET agent = ?
WHERE username = ? AND api_url = ?
""",
(agent, username, api_url),
)
self.conn.commit()
def delete_command(self, username: str, agent: str) -> None:
self.c.execute(
"""
DELETE FROM lc_commands
WHERE username = ? AND agent = ?
""",
(username, agent),
)
self.conn.commit()
def delete_commands(self, username: str) -> None:
self.c.execute(
"""
DELETE FROM lc_commands
WHERE username = ?
""",
(username,),
)
self.conn.commit()

View file

@ -1,121 +0,0 @@
import asyncio
import json
import os
from pathlib import Path
import signal
import sys
from bot import Bot
from log import getlogger
logger = getlogger()
async def main():
need_import_keys = False
config_path = Path(os.path.dirname(__file__)).parent / "config.json"
if os.path.isfile(config_path):
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"),
user_id=config.get("user_id"),
password=config.get("password"),
access_token=config.get("access_token"),
device_id=config.get("device_id"),
room_id=config.get("room_id"),
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=config.get("max_tokens"),
top_p=config.get("top_p"),
presence_penalty=config.get("presence_penalty"),
frequency_penalty=config.get("frequency_penalty"),
reply_count=config.get("reply_count"),
system_prompt=config.get("system_prompt"),
temperature=config.get("temperature"),
lc_admin=config.get("lc_admin"),
image_generation_endpoint=config.get("image_generation_endpoint"),
image_generation_backend=config.get("image_generation_backend"),
image_generation_size=config.get("image_generation_size"),
sdwui_steps=config.get("sdwui_steps"),
sdwui_sampler_name=config.get("sdwui_sampler_name"),
sdwui_cfg_scale=config.get("sdwui_cfg_scale"),
image_format=config.get("image_format"),
timeout=config.get("timeout"),
)
if (
config.get("import_keys_path")
and config.get("import_keys_password") is not None
):
need_import_keys = True
else:
matrix_bot = Bot(
homeserver=os.environ.get("HOMESERVER"),
user_id=os.environ.get("USER_ID"),
password=os.environ.get("PASSWORD"),
access_token=os.environ.get("ACCESS_TOKEN"),
device_id=os.environ.get("DEVICE_ID"),
room_id=os.environ.get("ROOM_ID"),
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", 4000)),
top_p=float(os.environ.get("TOP_P", 1.0)),
presence_penalty=float(os.environ.get("PRESENCE_PENALTY", 0.0)),
frequency_penalty=float(os.environ.get("FREQUENCY_PENALTY", 0.0)),
reply_count=int(os.environ.get("REPLY_COUNT", 1)),
system_prompt=os.environ.get("SYSTEM_PROMPT"),
temperature=float(os.environ.get("TEMPERATURE", 0.8)),
lc_admin=os.environ.get("LC_ADMIN"),
image_generation_endpoint=os.environ.get("IMAGE_GENERATION_ENDPOINT"),
image_generation_backend=os.environ.get("IMAGE_GENERATION_BACKEND"),
image_generation_size=os.environ.get("IMAGE_GENERATION_SIZE"),
sdwui_steps=int(os.environ.get("SDWUI_STEPS", 20)),
sdwui_sampler_name=os.environ.get("SDWUI_SAMPLER_NAME"),
sdwui_cfg_scale=float(os.environ.get("SDWUI_CFG_SCALE", 7)),
image_format=os.environ.get("IMAGE_FORMAT"),
timeout=float(os.environ.get("TIMEOUT", 120.0)),
)
if (
os.environ.get("IMPORT_KEYS_PATH")
and os.environ.get("IMPORT_KEYS_PASSWORD") is not None
):
need_import_keys = True
await matrix_bot.login()
if need_import_keys:
logger.info("start import_keys process, this may take a while...")
await matrix_bot.import_keys()
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)),
)
if matrix_bot.client.should_upload_keys:
await matrix_bot.client.keys_upload()
await sync_task
if __name__ == "__main__":
logger.info("matrix chatgpt bot start.....")
asyncio.run(main())

View file

@ -1,63 +0,0 @@
import markdown
from log import getlogger
from nio import AsyncClient
logger = getlogger()
async def send_room_message(
client: AsyncClient,
room_id: str,
reply_message: str,
sender_id: str = "",
user_message: str = "",
reply_to_event_id: str = "",
) -> None:
if reply_to_event_id == "":
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 = "> <" + sender_id + "> " + user_message + "\n\n" + reply_message
format = r"org.matrix.custom.html"
formatted_body = (
r'<mx-reply><blockquote><a href="https://matrix.to/#/'
+ room_id
+ r"/"
+ reply_to_event_id
+ r'">In reply to</a> <a href="https://matrix.to/#/'
+ sender_id
+ r'">'
+ sender_id
+ r"</a><br>"
+ user_message
+ r"</blockquote></mx-reply>"
+ markdown.markdown(
reply_message,
extensions=["nl2br", "tables", "fenced_code"],
)
)
content = {
"msgtype": "m.text",
"body": body,
"format": format,
"formatted_body": formatted_body,
"m.relates_to": {"m.in_reply_to": {"event_id": reply_to_event_id}},
}
try:
await client.room_send(
room_id,
message_type="m.room.message",
content=content,
ignore_unverified_devices=True,
)
await client.room_typing(room_id, typing_state=False)
except Exception as e:
logger.error(e)

View file

@ -1,26 +1,15 @@
""" """
Code derived from https://github.com/acheong08/ChatGPT/blob/main/src/revChatGPT/V3.py Code derived from: https://github.com/acheong08/ChatGPT/blob/main/src/revChatGPT/V3.py
A simple wrapper for the official ChatGPT API
""" """
import json import json
import os
from typing import AsyncGenerator from typing import AsyncGenerator
from tenacity import retry, wait_random_exponential, stop_after_attempt
import httpx import httpx
import requests
import tiktoken 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: class Chatbot:
""" """
Official ChatGPT API Official ChatGPT API
@ -28,48 +17,29 @@ class Chatbot:
def __init__( def __init__(
self, self,
aclient: httpx.AsyncClient,
api_key: str, api_key: str,
api_url: str = None, engine: str = os.environ.get("GPT_ENGINE") or "gpt-3.5-turbo",
engine: str = None, proxy: str = None,
timeout: float = None, timeout: float = None,
max_tokens: int = None, max_tokens: int = None,
temperature: float = 0.8, temperature: float = 0.5,
top_p: float = 1.0, top_p: float = 1.0,
presence_penalty: float = 0.0, presence_penalty: float = 0.0,
frequency_penalty: float = 0.0, frequency_penalty: float = 0.0,
reply_count: int = 1, reply_count: int = 1,
truncate_limit: int = None, system_prompt: str = "You are ChatGPT, a large language model trained by OpenAI. Respond conversationally",
system_prompt: str = None,
) -> None: ) -> None:
""" """
Initialize Chatbot with API key (from https://platform.openai.com/account/api-keys) Initialize Chatbot with API key (from https://platform.openai.com/account/api-keys)
""" """
self.engine: str = engine or "gpt-3.5-turbo" self.engine: str = engine
self.api_key: str = api_key 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
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 ( self.max_tokens: int = max_tokens or (
31000 31000 if engine == "gpt-4-32k" else 7000 if engine == "gpt-4" else 4000
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 ( self.truncate_limit: int = (
30500 30500 if engine == "gpt-4-32k" else 6500 if engine == "gpt-4" else 3500
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.temperature: float = temperature
self.top_p: float = top_p self.top_p: float = top_p
@ -77,8 +47,32 @@ class Chatbot:
self.frequency_penalty: float = frequency_penalty self.frequency_penalty: float = frequency_penalty
self.reply_count: int = reply_count self.reply_count: int = reply_count
self.timeout: float = timeout self.timeout: float = timeout
self.proxy = proxy
self.session = requests.Session()
self.session.proxies.update(
{
"http": proxy,
"https": proxy,
},
)
proxy = (
proxy or os.environ.get(
"all_proxy") or os.environ.get("ALL_PROXY") or None
)
self.aclient = aclient if proxy:
if "socks5h" not in proxy:
self.aclient = httpx.AsyncClient(
follow_redirects=True,
proxies=proxy,
timeout=timeout,
)
else:
self.aclient = httpx.AsyncClient(
follow_redirects=True,
proxies=proxy,
timeout=timeout,
)
self.conversation: dict[str, list[dict]] = { self.conversation: dict[str, list[dict]] = {
"default": [ "default": [
@ -89,9 +83,6 @@ class Chatbot:
], ],
} }
if self.get_token_count("default") > self.max_tokens:
raise Exception("System prompt is too long")
def add_to_conversation( def add_to_conversation(
self, self,
message: str, message: str,
@ -117,26 +108,30 @@ class Chatbot:
else: else:
break 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: def get_token_count(self, convo_id: str = "default") -> int:
""" """
Get token count Get token count
""" """
_engine = self.engine if self.engine not in [
if self.engine not in ENGINES: "gpt-3.5-turbo",
# use gpt-3.5-turbo to caculate token "gpt-3.5-turbo-0301",
_engine = "gpt-3.5-turbo" "gpt-4",
"gpt-4-0314",
"gpt-4-32k",
"gpt-4-32k-0314",
]:
raise NotImplementedError("Unsupported engine {self.engine}")
tiktoken.model.MODEL_TO_ENCODING["gpt-4"] = "cl100k_base" tiktoken.model.MODEL_TO_ENCODING["gpt-4"] = "cl100k_base"
encoding = tiktoken.encoding_for_model(_engine) encoding = tiktoken.encoding_for_model(self.engine)
num_tokens = 0 num_tokens = 0
for message in self.conversation[convo_id]: for message in self.conversation[convo_id]:
# every message follows <im_start>{role/name}\n{content}<im_end>\n # every message follows <im_start>{role/name}\n{content}<im_end>\n
num_tokens += 5 num_tokens += 5
for key, value in message.items(): for key, value in message.items():
if value: num_tokens += len(encoding.encode(value))
num_tokens += len(encoding.encode(value))
if key == "name": # if there's a name, the role is omitted 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 # role is always required and always 1 token
num_tokens += 5 # every reply is primed with <im_start>assistant num_tokens += 5 # every reply is primed with <im_start>assistant
@ -148,15 +143,13 @@ class Chatbot:
""" """
return self.max_tokens - self.get_token_count(convo_id) return self.max_tokens - self.get_token_count(convo_id)
async def ask_stream_async( def ask_stream(
self, self,
prompt: str, prompt: str,
role: str = "user", role: str = "user",
convo_id: str = "default", convo_id: str = "default",
model: str = None,
pass_history: bool = True,
**kwargs, **kwargs,
) -> AsyncGenerator[str, None]: ):
""" """
Ask a question Ask a question
""" """
@ -166,13 +159,14 @@ class Chatbot:
self.add_to_conversation(prompt, "user", convo_id=convo_id) self.add_to_conversation(prompt, "user", convo_id=convo_id)
self.__truncate_conversation(convo_id=convo_id) self.__truncate_conversation(convo_id=convo_id)
# Get response # Get response
async with self.aclient.stream( response = self.session.post(
"post", os.environ.get(
self.api_url, "API_URL") or "https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {kwargs.get('api_key', self.api_key)}"}, headers={
"Authorization": f"Bearer {kwargs.get('api_key', self.api_key)}"},
json={ json={
"model": model or self.engine, "model": self.engine,
"messages": self.conversation[convo_id] if pass_history else [prompt], "messages": self.conversation[convo_id],
"stream": True, "stream": True,
# kwargs # kwargs
"temperature": kwargs.get("temperature", self.temperature), "temperature": kwargs.get("temperature", self.temperature),
@ -187,90 +181,63 @@ class Chatbot:
), ),
"n": kwargs.get("n", self.reply_count), "n": kwargs.get("n", self.reply_count),
"user": role, "user": role,
"max_tokens": min( "max_tokens": self.get_max_tokens(convo_id=convo_id),
self.get_max_tokens(convo_id=convo_id),
kwargs.get("max_tokens", self.max_tokens),
),
}, },
timeout=kwargs.get("timeout", self.timeout), timeout=kwargs.get("timeout", self.timeout),
) as response: stream=True,
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
async def ask_async_v2( response_role: str = None
full_response: str = ""
for line in response.iter_lines():
if not line:
continue
# Remove "data: "
line = line.decode("utf-8")[6:]
if line == "[DONE]":
break
resp: dict = json.loads(line)
choices = resp.get("choices")
if not choices:
continue
delta = choices[0].get("delta")
if not delta:
continue
if "role" in delta:
response_role = delta["role"]
if "content" in delta:
content = delta["content"]
full_response += content
yield content
self.add_to_conversation(
full_response, response_role, convo_id=convo_id)
async def ask_stream_async(
self, self,
prompt: str, prompt: str,
role: str = "user", role: str = "user",
convo_id: str = "default", convo_id: str = "default",
model: str = None,
pass_history: bool = True,
**kwargs, **kwargs,
) -> str: ) -> AsyncGenerator[str, None]:
"""
Ask a question
"""
# Make conversation if it doesn't exist # Make conversation if it doesn't exist
if convo_id not in self.conversation: if convo_id not in self.conversation:
self.reset(convo_id=convo_id, system_prompt=self.system_prompt) self.reset(convo_id=convo_id, system_prompt=self.system_prompt)
self.add_to_conversation(prompt, "user", convo_id=convo_id) self.add_to_conversation(prompt, "user", convo_id=convo_id)
self.__truncate_conversation(convo_id=convo_id) self.__truncate_conversation(convo_id=convo_id)
# Get response # Get response
response = await self.aclient.post( async with self.aclient.stream(
url=self.api_url, "post",
headers={"Authorization": f"Bearer {kwargs.get('api_key', self.api_key)}"}, os.environ.get(
"API_URL") or "https://api.openai.com/v1/chat/completions",
headers={
"Authorization": f"Bearer {kwargs.get('api_key', self.api_key)}"},
json={ json={
"model": model or self.engine, "model": self.engine,
"messages": self.conversation[convo_id] if pass_history else [prompt], "messages": self.conversation[convo_id],
"stream": True,
# kwargs # kwargs
"temperature": kwargs.get("temperature", self.temperature), "temperature": kwargs.get("temperature", self.temperature),
"top_p": kwargs.get("top_p", self.top_p), "top_p": kwargs.get("top_p", self.top_p),
@ -284,18 +251,75 @@ class Chatbot:
), ),
"n": kwargs.get("n", self.reply_count), "n": kwargs.get("n", self.reply_count),
"user": role, "user": role,
"max_tokens": min( "max_tokens": self.get_max_tokens(convo_id=convo_id),
self.get_max_tokens(convo_id=convo_id),
kwargs.get("max_tokens", self.max_tokens),
),
}, },
timeout=kwargs.get("timeout", self.timeout), timeout=kwargs.get("timeout", self.timeout),
) ) as response:
resp = response.json() if response.status_code != 200:
full_response = resp["choices"][0]["message"]["content"] await response.aread()
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)
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( self.add_to_conversation(
full_response, resp["choices"][0]["message"]["role"], convo_id=convo_id full_response, response_role, convo_id=convo_id)
async def ask_async(
self,
prompt: str,
role: str = "user",
convo_id: str = "default",
**kwargs,
) -> str:
"""
Non-streaming ask
"""
response = self.ask_stream_async(
prompt=prompt,
role=role,
convo_id=convo_id,
**kwargs,
) )
full_response: str = "".join([r async for r in response])
return full_response
def ask(
self,
prompt: str,
role: str = "user",
convo_id: str = "default",
**kwargs,
) -> str:
"""
Non-streaming ask
"""
response = self.ask_stream(
prompt=prompt,
role=role,
convo_id=convo_id,
**kwargs,
)
full_response: str = "".join(response)
return full_response return full_response
def reset(self, convo_id: str = "default", system_prompt: str = None) -> None: def reset(self, convo_id: str = "default", system_prompt: str = None) -> None:
@ -305,40 +329,3 @@ class Chatbot:
self.conversation[convo_id] = [ self.conversation[convo_id] = [
{"role": "system", "content": system_prompt or self.system_prompt}, {"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:
response = await self.aclient.post(
url=self.api_url,
json={
"model": model or self.engine,
"messages": [
{
"role": role,
"content": 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),
)
resp = response.json()
return resp["choices"][0]["message"]["content"]