├── .dockerignore ├── .github └── FUNDING.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README-zh.md ├── README.md ├── ai ├── __init__.py ├── azure.py └── openai.py ├── buttons ├── __init__.py ├── help.py ├── image.py ├── inline.py ├── language.py ├── others.py ├── role.py ├── start.py ├── statistics.py └── templates.py ├── chat ├── __init__.py ├── ai.py └── handler.py ├── chat_modes.yml ├── config.example.yaml ├── config.py ├── db ├── MySqlConn.py ├── __init__.py ├── database.sql └── docker-compose.yaml ├── demo.gif ├── docker-compose.yaml ├── main.py ├── requirements.txt └── tests ├── __init__.py ├── test_image.py ├── test_openai_client.py └── test_zaure_client.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | venv 3 | config.yaml 4 | __pycache__ 5 | *.log 6 | conversationbot 7 | .DS_Store -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: Cancellara 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | venv 3 | config.yaml 4 | config.yaml.* 5 | __pycache__ 6 | *.log 7 | conversationbot 8 | .DS_Store 9 | test.*.py 10 | data -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | LABEL org.opencontainers.image.source=https://github.com/V-know/ChatGPT-Telegram-Bot 4 | 5 | ARG APP_HOME=/app 6 | WORKDIR $APP_HOME 7 | 8 | ADD . $APP_HOME 9 | 10 | RUN pip install cryptography && pip install -r $APP_HOME/requirements.txt && apt-get clean -y 11 | RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ 12 | echo "Asia/Shanghai" > /etc/timezone 13 | 14 | ENTRYPOINT ["python", "main.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Gavin Wei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 定义变量 2 | VENV := venv 3 | PYTHON := $(VENV)/bin/python 4 | PIP := $(VENV)/bin/pip 5 | REQUIREMENTS := requirements.txt 6 | 7 | # 定义默认目标 8 | .DEFAULT_GOAL := help 9 | 10 | # 安装依赖项 11 | install: $(VENV)/bin/activate 12 | $(PIP) install -r $(REQUIREMENTS) 13 | 14 | # 运行应用程序 15 | run: $(VENV)/bin/activate 16 | $(PYTHON) main.py 17 | 18 | image: $(VENV)/bin/activate 19 | docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/v-know/chatgpt-telegram-bot:latest --push . 20 | 21 | preflight: $(VENV)/bin/activate 22 | docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/v-know/chatgpt-telegram-bot:preflight --push . 23 | 24 | # 显示帮助信息 25 | help: 26 | @echo "Available targets:" 27 | @echo " install 安装依赖项" 28 | @echo " run 运行应用程序" -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Telegram Bot 2 | 3 |  4 | [](https://github.com/python-telegram-bot/python-telegram-bot/releases/tag/v20.3) 5 |  6 | [](https://openai.com/) 7 | [](LICENSE) 8 | [](https://t.me/RoboAceBot) 9 | 10 | [English](README.md) | 中文 11 | 12 | 一个拥有丝滑AI体验的Telegram Bot 13 | 14 | ## ⚡Feature 15 | 16 | [✓] 同时支持Azure OpenAI和原生OpenAI接口 17 | 18 | [✓] 实时(流式)返回AI响应的答案,体验更快捷、更丝滑 19 | 20 | [✓] 基于OpenAI DALL·E 3的文生图 21 | 22 | [✓] 预设15种Bot身份,可快速切换 23 | 24 | [✓] 支持自定义Bot身份,满足个性化需求 25 | 26 | [✓] 支持上下文件内容一键清空,随时重开会话 27 | 28 | [✓] Telegram Bot 原生按钮支持,直观快捷实现需要功能 29 | 30 | [✓] 用户等级划分,不同等级享有不同单次会话Token数量、上下文数量和会话频率 31 | 32 | [✓] 支持中/英双语切换 33 | 34 | [✓] 容器化 35 | 36 | [✓] More ... 37 | 38 |  39 | 40 | ## 👨💻TODO 41 | 42 | [x] 允许用户在Bot中使用自己的OpenAI Key,以获得更多自由 43 | 44 | [x] 完善ErrorHandler 45 | 46 | ## 🤖快速体验 47 | 48 | Telegram Bot: [RoboAceBot](https://t.me/RoboAceBot) 49 | 50 | ## 🛠️部署 51 | 52 | ### 安装依赖 53 | 54 | ```shell 55 | pip install -r requirements.txt 56 | ``` 57 | 58 | ### 配置数据库 59 | 60 | #### 安装数据库 61 | 62 | 你可以使用下面的命令快速创建本地MySQL数据库 63 | 64 | ```shell 65 | dcker-compose up -d -f db/docker-dompose.yaml 66 | ``` 67 | 68 | #### 初始化数据库 69 | 70 | ```shell 71 | mysql -uusername -p -e "source db/database.sql" 72 | ``` 73 | 74 | ### 添加配置 75 | 76 | 需要的所有配置都在`config.yaml`中,文件格式内容,请参考`config.yaml.example` 77 | 78 | | Parameter | Optional | Description | 79 | |---------------------|----------|------------------------------------------------------------------------------------------------------------| 80 | | `BOT`.`TOKEN` | No | 从[@botFather](https://t.me/BotFather)创建bot并获取Token | 81 | | `DEVELOPER_CHAT_ID` | No | bot出错时,接收信息的TG帐号ID, ID可以从[@get_id_bot](https://t.me/get_id_bot) 获取 | 82 | | `MYSQL` | No | MySQL连接相关的参数 | 83 | | `TIME_SPAN` | No | 计算rate limit所用的时间窗口大小,单位:分钟 | 84 | | `RATE_LIMIT` | No | `key`为用户等级,`value`为TIME_SPAN时间内可以聊天的最大数量 | 85 | | `CONTEXT_COUNT` | No | `key`为用户等级,`value`为每次聊天所包含的上下文数量 | 86 | | `MAX_TOKEN` | No | `key`为用户等级, `value`为每次聊天AI返回节点的最大Token数 | 87 | | `AI`.`TYPE` | Yes | 使用的是AI类型,有`openai`和`azure`两个选项,默认为`openai` | 88 | | `AI`.`BASE` | Yes | 从 Azure 门户检查资源时,可在“密钥和终结点”部分中找到此值。 或者,可以在“Azure OpenAI Studio”>“操场”>“代码视图”中找到该值, 仅当`AI`.`TYPE`为`zaure`里需要设置 | 89 | | `AI`.`VERSION` | Yes | Azure OpenAI的版本号, 仅当`AI`.`TYPE`为`zaure`时需要设置 | 90 | | `AI`.`MODEL` | Yes | Azure OpenAI的Deployment名, 或OpenAI所使用的 Model 名 | 91 | 92 | 如果你使用的是Azure的OpenAI,你可在这个链接里获取所需的所有内容: 93 | 94 | [开始通过 Azure OpenAI 服务使用 ChatGPT 和 GPT-4](https://learn.microsoft.com/zh-cn/azure/cognitive-services/openai/chatgpt-quickstart?pivots=programming-language-python&tabs=command-line) 95 | 96 | ## 🚀启动 97 | 98 | ```shell 99 | python main.py | tee >> debug.log 100 | ``` 101 | 102 | ### Docker build & Run 103 | 104 | ```shell 105 | docker run --rm --name chatgpt-telegram-bot -v ./config.yaml:/app/config.yaml ghcr.io/v-know/chatgpt-telegram-bot:latest 106 | ``` 107 | 108 | ### Docker Compose 109 | 110 | ```shell 111 | docker-compose up -d 112 | ``` 113 | ## ❤️写在最后 114 | 115 | 希望本项目在给你丝滑AI体验的同时,能帮助更多人接触并开始创建和使用自己的Telegram Bot 116 | 117 | ## Star History 118 | 119 | [](https://star-history.com/#v-know/chatgpt-telegram-bot&Date) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Telegram Bot 2 | 3 |  4 | [](https://github.com/python-telegram-bot/python-telegram-bot/releases/tag/v20.3) 5 |  6 | [](https://openai.com/) 7 | [](LICENSE) 8 | [](https://t.me/RoboAceBot) 9 | 10 | English | [中文](README-zh.md) 11 | 12 | A Telegram bot with a silky smooth AI experience. 13 | 14 | ## ⚡Feature 15 | 16 | [✓] Support for both Azure OpenAI and native OpenAI. 17 | 18 | [✓] Real-time (streaming) response to AI, with faster and smoother experience. 19 | 20 | [✓] Generate picture based on OpenAI DALL·E 3 model. 21 | 22 | [✓] 15 preset bot identities that can be quickly switched. 23 | 24 | [✓] Support for custom bot identities to meet personalized needs. 25 | 26 | [✓] Support to clear the contents of the chat with a single click, and restart the conversation at any time. 27 | 28 | [✓]Native Telegram bot button support, making it easy and intuitive to implement required functions. 29 | 30 | [✓] User level division, with different levels enjoying different single session token numbers, context numbers, and session frequencies. 31 | 32 | [✓] Support English and Chinese on UI 33 | 34 | [✓] Containerization. 35 | 36 | [✓] More... 37 | 38 |  39 | 40 | ## 👨💻TODO 41 | 42 | [x] Allow users to use their own OpenAI Key in the bot to gain more freedom. 43 | 44 | [x] Improve ErrorHandler. 45 | 46 | ## 🤖Quick Experience 47 | 48 | Telegram Bot: [RoboAceBot](https://t.me/RoboAceBot) 49 | 50 | ## 🛠️Deployment 51 | 52 | ### Install Dependencies 53 | 54 | ```shell 55 | pip install -r requirements.txt 56 | ``` 57 | 58 | ### Configure Database 59 | 60 | #### Install Database 61 | 62 | You can quickly create a local MySQL database using: 63 | 64 | ```shell 65 | docker-compose up -d -f db/docker-compose.yaml 66 | ``` 67 | 68 | #### Initialize Database 69 | 70 | ```shell 71 | mysql -uusername -p -e "source db/database.sql" 72 | ``` 73 | 74 | ### Add Configuration 75 | 76 | All the required configurations are in `config.yaml`, please refer to `config.yaml.example` for file format and content. 77 | 78 | | Parameter | Optional | Description | 79 | |---------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 80 | | `BOT`.`TOKEN` | No | Create a bot from [@botFather](https://t.me/BotFather) and get the Token. | 81 | | `DEVELOPER_CHAT_ID` | No | Telegram account ID that receives messages when the bot encounters an error. You can use [@get_id_bot](https://t.me/get_id_bot) to get your ID. | 82 | | `MYSQL` | No | Parameters related to MySQL connection. | 83 | | `TIME_SPAN` | No | The time window size used to calculate the ratelimit, in minutes. | 84 | | `RATE_LIMIT` | No | `key` is the user level, and `value` is the maximum number of chats that can be made within the TIME_SPAN time period. | 85 | | `CONTEXT_COUNT` | No | `key` is the user level, and `value` is the number of contexts included in each chat. | 86 | | `MAX_TOKEN` | No | `key` is the user level, and `value` is the maximum number of tokens returned by the AI per chat. | 87 | | `AI`.`TYPE` | Yes | The type of AI used, with two options: `openai` and `azure`. The default is `openai`. | 88 | | `AI`.`BASE` | Yes | When checking resources from the Azure portal, this value can be found in the "Keys and Endpoints" section. Alternatively, this value can be found in "Azure OpenAI Studio" > "Playground" > "Code View". Only needs to be set when `AI`.`TYPE` is `azure`. | 89 | | `AI`.`VERSION` | Yes | The version number of Azure OpenAI, only needs to be set when `AI`.`TYPE`is `azure`. | 90 | | `AI`.`MODEL` | Yes | The deployment name of Azure OpenAI, or the Model of OpenAI. | 91 | 92 | If you are using Azure's OpenAI, you can obtain all the required content at this link: 93 | 94 | [Get started using ChatGPT and GPT-4 with Azure OpenAI Services](https://learn.microsoft.com/zh-cn/azure/cognitive-services/openai/chatgpt-quickstart?pivots=programming-language-python&tabs=command-line) 95 | 96 | ## 🚀Start 97 | 98 | ```shell 99 | python main.py | tee >> debug.log 100 | ``` 101 | 102 | ### Docker build & Run 103 | 104 | ```shell 105 | docker run --rm --name chatgpt-telegram-bot -v ./config.yaml:/app/config.yaml ghcr.io/v-know/chatgpt-telegram-bot:latest 106 | ``` 107 | 108 | ### Docker Compose 109 | 110 | ```shell 111 | docker-compose up -d 112 | ``` 113 | 114 | ## ❤️In Conclusion 115 | 116 | I hope this project can provide you with a smooth AI experience and help more people create and use their own Telegram bots. 117 | 118 | ## Star History 119 | 120 | [](https://star-history.com/#v-know/chatgpt-telegram-bot&Date) -------------------------------------------------------------------------------- /ai/__init__.py: -------------------------------------------------------------------------------- 1 | from db.MySqlConn import config 2 | 3 | OPENAI_CHAT_COMPLETION_OPTIONS = { 4 | "temperature": 0.7, 5 | "top_p": 1, 6 | "frequency_penalty": 0, 7 | "presence_penalty": 0, 8 | "stream": True, 9 | "stop": None, 10 | "model": config["AI"]["MODEL"] 11 | } 12 | -------------------------------------------------------------------------------- /ai/azure.py: -------------------------------------------------------------------------------- 1 | from openai import AzureOpenAI 2 | from db.MySqlConn import config 3 | from ai import OPENAI_CHAT_COMPLETION_OPTIONS 4 | 5 | 6 | class AzureAIClient: 7 | def __init__(self): 8 | self.open_ai_config = { 9 | 'api_key': config["AI"]["TOKEN"], 10 | 'azure_endpoint': config["AI"]["BASE"], 11 | 'api_version': config["AI"]["VERSION"] 12 | } 13 | 14 | self.client = AzureOpenAI(**self.open_ai_config) 15 | 16 | def generate_image(self, prompt) -> str: 17 | response = self.client.images.generate( 18 | model="dalle3", 19 | prompt=prompt, 20 | size="1024x1024", 21 | quality="standard", 22 | n=1 23 | ) 24 | 25 | image_url = response.data[0].url 26 | return image_url 27 | 28 | def chat_completions(self, messages: list): 29 | answer = "" 30 | completion = self.client.chat.completions.create( 31 | model=OPENAI_CHAT_COMPLETION_OPTIONS["model"], 32 | messages=messages 33 | ) 34 | # print(completion.choices[0].message) 35 | return completion 36 | -------------------------------------------------------------------------------- /ai/openai.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | from db.MySqlConn import config 3 | from ai import OPENAI_CHAT_COMPLETION_OPTIONS 4 | 5 | 6 | class OpenAIClient: 7 | def __init__(self): 8 | self.open_ai_config = {'api_key': config["AI"]["TOKEN"]} 9 | self.client = OpenAI(**self.open_ai_config) 10 | 11 | def generate_image(self, prompt) -> str: 12 | response = self.client.images.generate( 13 | model="dall-e-3", 14 | prompt=prompt, 15 | size="1024x1024", 16 | quality="standard", 17 | n=1 18 | ) 19 | 20 | image_url = response.data[0].url 21 | return image_url 22 | 23 | # For testing purposes 24 | def chat_completions(self, messages: list): 25 | answer = "" 26 | completion = self.client.chat.completions.create( 27 | model=OPENAI_CHAT_COMPLETION_OPTIONS["model"], 28 | messages=messages 29 | ) 30 | # print(completion.choices[0].message) 31 | return completion 32 | -------------------------------------------------------------------------------- /buttons/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def get_project_root() -> Path: 5 | return Path(__file__).resolve().parent.parent 6 | -------------------------------------------------------------------------------- /buttons/help.py: -------------------------------------------------------------------------------- 1 | from telegram import Update 2 | from telegram.ext import ContextTypes 3 | from config import CHOOSING 4 | from db.MySqlConn import Mysql 5 | from buttons.templates import say_help 6 | 7 | 8 | async def helper(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: 9 | user_id = update.effective_user.id 10 | mysql = Mysql() 11 | user = mysql.getOne("select * from users where user_id=%s", user_id) 12 | mysql.end() 13 | await update.message.reply_text(say_help[user["lang"]], parse_mode="Markdown", disable_web_page_preview=True) 14 | return CHOOSING 15 | -------------------------------------------------------------------------------- /buttons/image.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import aiohttp 3 | import time 4 | from telegram import Update 5 | from telegram.ext import ContextTypes 6 | from db.MySqlConn import Mysql 7 | 8 | from buttons import get_project_root 9 | from buttons.templates import image, image_limit, cancel_notification 10 | from chat.ai import GenerateImage 11 | from config import ( 12 | reply_markup, 13 | cancel_markup, 14 | CHOOSING, 15 | image_rate_limit, 16 | notification_channel, 17 | TYPING_IMAGE_PROMPT) 18 | 19 | 20 | async def download_image(image_url: str, save_path: str) -> None: 21 | async with aiohttp.ClientSession() as session: 22 | async with session.get(image_url) as response: 23 | if response.status == 200: 24 | with open(save_path, 'wb') as f: 25 | f.write(await response.read()) 26 | 27 | 28 | async def set_image_prompt(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: 29 | user_id = update.effective_user.id 30 | mysql = Mysql() 31 | user = mysql.getOne("select * from users where user_id=%s", user_id) 32 | mysql.end() 33 | await update.message.reply_text(text=image[user["lang"]], 34 | parse_mode='Markdown', disable_web_page_preview=True, reply_markup=cancel_markup) 35 | return TYPING_IMAGE_PROMPT 36 | 37 | 38 | async def set_image_prompt_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: 39 | user_id = update.effective_user.id 40 | nick_name = update.effective_user.full_name 41 | mysql = Mysql() 42 | logged_in_user = mysql.getOne("select * from users where user_id=%s", user_id) 43 | 44 | # Check rate limit 45 | today = datetime.now().date() 46 | start_of_day = datetime.combine(today, datetime.min.time()) 47 | end_of_day = datetime.combine(today, datetime.max.time()) 48 | call_count = mysql.getOne( 49 | "select count(*) as count from image_requests where user_id=%s and created_at between %s and %s", 50 | (user_id, start_of_day, end_of_day) 51 | ) 52 | 53 | image_prompt = update.message.text.strip() 54 | if image_prompt in ("取消", "取消重置", "🚫取消", "cancel", "reset", "🚫Cancel"): 55 | await update.message.reply_text( 56 | text=cancel_notification[logged_in_user["lang"]], reply_markup=reply_markup, parse_mode='Markdown') 57 | return CHOOSING 58 | else: 59 | level = logged_in_user.get("level") 60 | if call_count.get("count") >= image_rate_limit[level]: 61 | await update.message.reply_text( 62 | text=image_limit[logged_in_user["lang"]], reply_markup=reply_markup, parse_mode='Markdown') 63 | return CHOOSING 64 | 65 | placeholder_message = await update.message.reply_text("...") 66 | image_url = await GenerateImage(image_prompt) 67 | chat_id = update.effective_chat.id 68 | 69 | await context.bot.edit_message_text("Here is your generated image:", chat_id=placeholder_message.chat_id, 70 | message_id=placeholder_message.message_id) 71 | await context.bot.send_photo(chat_id=chat_id, photo=image_url, reply_markup=reply_markup, parse_mode='Markdown') 72 | 73 | project_root = get_project_root() 74 | image_name = f'{nick_name}-{time.strftime("%Y%m%d-%H%M%S")}.jpg' 75 | save_path = f'{project_root}/data/pictures/{image_name}' 76 | 77 | date_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 78 | mysql.insertOne( 79 | "insert into image_requests (user_id, prompt, image_name, created_at) values (%s, %s, %s, %s)", 80 | (user_id, image_prompt, image_name, date_time)) 81 | 82 | mysql.end() 83 | if notification_channel: 84 | msg = f"#U{user_id} {nick_name}: {image_prompt}" 85 | await context.bot.send_photo(chat_id=notification_channel, photo=image_url, caption=msg, ) 86 | await download_image(image_url, save_path) 87 | return CHOOSING 88 | -------------------------------------------------------------------------------- /buttons/inline.py: -------------------------------------------------------------------------------- 1 | from telegram.ext import ContextTypes 2 | from telegram.constants import ParseMode 3 | from telegram.error import BadRequest 4 | from telegram import ( 5 | Update, 6 | InlineKeyboardMarkup, 7 | InlineKeyboardButton) 8 | import yaml 9 | import time 10 | 11 | from db.MySqlConn import Mysql 12 | from config import reply_markup 13 | 14 | 15 | async def show_chat_modes_handle(update: Update, context: ContextTypes.DEFAULT_TYPE): 16 | text, inline_reply_markup = get_chat_mode_menu(0) 17 | await update.message.reply_text(text, reply_markup=inline_reply_markup, parse_mode=ParseMode.HTML) 18 | 19 | 20 | with open("chat_modes.yml") as f: 21 | chat_modes = yaml.load(f, Loader=yaml.FullLoader) 22 | 23 | 24 | def get_chat_mode_menu(page_index: int): 25 | n_chat_modes_per_page = 5 26 | text = f"Select chat mode ({len(chat_modes)} modes available):" 27 | 28 | # buttons 29 | chat_mode_keys = list(chat_modes.keys()) 30 | page_chat_mode_keys = chat_mode_keys[page_index * n_chat_modes_per_page:(page_index + 1) * n_chat_modes_per_page] 31 | 32 | keyboard = [] 33 | for chat_mode_key in page_chat_mode_keys: 34 | name = chat_modes[chat_mode_key]["name"] 35 | keyboard.append([InlineKeyboardButton(name, callback_data=f"set_chat_mode|{chat_mode_key}")]) 36 | 37 | # pagination 38 | if len(chat_mode_keys) > n_chat_modes_per_page: 39 | is_first_page = (page_index == 0) 40 | is_last_page = ((page_index + 1) * n_chat_modes_per_page >= len(chat_mode_keys)) 41 | 42 | if is_first_page: 43 | keyboard.append([ 44 | InlineKeyboardButton("»", callback_data=f"show_chat_modes|{page_index + 1}") 45 | ]) 46 | elif is_last_page: 47 | keyboard.append([ 48 | InlineKeyboardButton("«", callback_data=f"show_chat_modes|{page_index - 1}"), 49 | ]) 50 | else: 51 | keyboard.append([ 52 | InlineKeyboardButton("«", callback_data=f"show_chat_modes|{page_index - 1}"), 53 | InlineKeyboardButton("»", callback_data=f"show_chat_modes|{page_index + 1}") 54 | ]) 55 | keyboard.append([InlineKeyboardButton("🚫Cancelled", callback_data="cancel")]) 56 | 57 | inline_reply_markup = InlineKeyboardMarkup(keyboard) 58 | 59 | return text, inline_reply_markup 60 | 61 | 62 | async def show_chat_modes_callback_handle(update: Update, context: ContextTypes.DEFAULT_TYPE): 63 | query = update.callback_query 64 | await query.answer() 65 | 66 | page_index = int(query.data.split("|")[1]) 67 | if page_index < 0: 68 | return 69 | 70 | text, markup = get_chat_mode_menu(page_index) 71 | try: 72 | await query.edit_message_text(text, reply_markup=markup, parse_mode=ParseMode.HTML) 73 | except BadRequest as e: 74 | if str(e).startswith("Message is not modified"): 75 | pass 76 | 77 | 78 | async def set_chat_mode_handle(update: Update, context: ContextTypes.DEFAULT_TYPE): 79 | user_id = update.callback_query.from_user.id 80 | nick_name = update.effective_user.full_name 81 | 82 | query = update.callback_query 83 | await query.answer() 84 | 85 | system_content = query.data.split("|")[1] 86 | 87 | mysql = Mysql() 88 | mysql.update("update users set system_content=%s, parse_mode=%s, nick_name=%s where user_id=%s", 89 | (chat_modes[system_content]['prompt_start'], chat_modes[system_content]["parse_mode"], nick_name, 90 | user_id)) 91 | reset_at = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 92 | mysql.update("update records set reset_at=%s where user_id=%s and reset_at is null", (reset_at, user_id)) 93 | mysql.end() 94 | 95 | await context.bot.send_message( 96 | update.callback_query.message.chat.id, 97 | f"{chat_modes[system_content]['welcome_message']}", 98 | parse_mode=ParseMode.HTML, reply_markup=reply_markup 99 | ) 100 | 101 | 102 | async def cancel_chat_mode_handle(update: Update, context: ContextTypes.DEFAULT_TYPE): 103 | user_id = update.effective_user.id 104 | mysql = Mysql() 105 | user = mysql.getOne("select * from users where user_id=%s", user_id) 106 | mysql.end() 107 | await context.bot.send_message( 108 | update.callback_query.message.chat.id, 109 | text="已取消。\n您可以继续向我提问了" if user[ 110 | "lang"] == "cn" else "Cancelled. \nYou can continue to ask me questions now.", 111 | parse_mode=ParseMode.MARKDOWN, reply_markup=reply_markup 112 | ) 113 | -------------------------------------------------------------------------------- /buttons/language.py: -------------------------------------------------------------------------------- 1 | from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton 2 | from telegram.ext import ContextTypes, CallbackContext 3 | 4 | from db.MySqlConn import Mysql 5 | from config import CHOOSING 6 | 7 | 8 | async def show_languages(update: Update, context: ContextTypes.DEFAULT_TYPE): 9 | keyboard = [ 10 | [ 11 | InlineKeyboardButton("English", callback_data='lang_en'), 12 | InlineKeyboardButton("中文", callback_data='lang_cn'), 13 | ] 14 | ] 15 | 16 | reply_markup = InlineKeyboardMarkup(keyboard) 17 | 18 | await update.message.reply_text('Please choose your language:', reply_markup=reply_markup) 19 | return CHOOSING 20 | 21 | 22 | async def show_languages_callback_handle(update: Update, context: ContextTypes.DEFAULT_TYPE): 23 | user_id = update.callback_query.from_user.id 24 | nick_name = update.effective_user.full_name 25 | 26 | query = update.callback_query 27 | # CallbackQueries need to be answered, even if no notification to the user is needed 28 | await query.answer() 29 | 30 | lang = query.data.split("_")[1] 31 | mysql = Mysql() 32 | mysql.update("update users set nick_name=%s, lang=%s where user_id=%s", (nick_name, lang, user_id)) 33 | mysql.end() 34 | 35 | await query.edit_message_text( 36 | text="Language changed to 🇬🇧 English" if lang == "en" else "语言已更改为 🇨🇳 中文") 37 | -------------------------------------------------------------------------------- /buttons/others.py: -------------------------------------------------------------------------------- 1 | from telegram.ext import ContextTypes, ConversationHandler 2 | from telegram import ( 3 | Update, 4 | ReplyKeyboardRemove) 5 | 6 | import time 7 | import json 8 | import html 9 | import openai 10 | import asyncio 11 | import traceback 12 | from typing import Dict 13 | 14 | from db.MySqlConn import config 15 | from buttons import get_project_root 16 | from config import ( 17 | TYPING_REPLY, 18 | logger) 19 | 20 | 21 | async def non_text_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: 22 | """Stores the photos and asks for a location.""" 23 | project_root = get_project_root() 24 | user = update.message.from_user 25 | if len(update.message.photo) != 0: 26 | await update.message.reply_text(text='Only text, thanks!') 27 | photo_file = await update.message.photo[-1].get_file() 28 | # can't get photo's name 29 | await photo_file.download_to_drive( 30 | f'{project_root}/data/photos/{user.name}-{time.strftime("%Y%m%d-%H%M%S")}.jpg') 31 | logger.info("Photo of %s: %s", user.first_name, 'user_photo.jpg') 32 | else: 33 | await update.message.reply_text(text='Only text, thanks!') 34 | if update.message.document: 35 | file = await update.message.document.get_file() 36 | await file.download_to_drive( 37 | f'{project_root}/data/documents/{user.name}-{time.strftime("%Y%m%d-%H%M%S")}.jpg') 38 | if update.message.video: 39 | video = await update.message.video.get_file() 40 | await video.download_to_drive( 41 | f'{project_root}/data/videos/{user.name}-{time.strftime("%Y%m%d-%H%M%S")}.jpg') 42 | return TYPING_REPLY 43 | 44 | 45 | def facts_to_str(user_data: Dict[str, str]) -> str: 46 | """Helper function for formatting the gathered user info.""" 47 | facts = [f'{key} - {value}' for key, value in user_data.items()] 48 | return "\n".join(facts).join(['\n', '\n']) 49 | 50 | 51 | async def done(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: 52 | await update.message.reply_text("Conversation canceled.", reply_markup=ReplyKeyboardRemove()) 53 | return ConversationHandler.END 54 | 55 | 56 | async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None: 57 | """Log the error and send a telegram message to notify the developer.""" 58 | # Log the error before we do anything else, so we can see it even if something breaks. 59 | logger.error("Exception while handling an update:", exc_info=context.error) 60 | 61 | # traceback.format_exception returns the usual python message about an exception, but as a 62 | # list of strings rather than a single string, so we have to join them together. 63 | tb_list = traceback.format_exception(None, context.error, context.error.__traceback__) 64 | tb_string = "".join(tb_list) 65 | 66 | # Build the message with some markup and additional information about what happened. 67 | # You might need to add some logic to deal with messages longer than the 4096-character limit. 68 | update_str = update.to_dict() if isinstance(update, Update) else str(update) 69 | message = ( 70 | f"An exception was raised while handling an update\n" 71 | f"
update = {html.escape(json.dumps(update_str, indent=2, ensure_ascii=False))}"
72 | "\n\n"
73 | f"error type = {type(context.error)}"
74 | f"context.chat_data = {html.escape(str(context.chat_data))}\n\n"
75 | f"context.user_data = {html.escape(str(context.user_data))}\n\n"
76 | f"prompt = {html.escape(str(update.message.text))}\n\n"
77 | f"{html.escape(tb_string)}"
78 | )
79 |
80 | # Finally, send the message
81 | error_reply = ""
82 | if type(context.error) in (openai.ErrorObject.error.InvalidRequestError, openai.BadRequestError):
83 | error_reply = "The response was filtered due to the prompt triggering OpenAI’s content management " \
84 | "policy. Please modify your prompt and retry. To learn more about our content filtering. "
85 | elif type(context.error) in [openai.ErrorObject.error.Timeout, asyncio.exceptions.TimeoutError]:
86 | error_reply = "Time out. Retry please!"
87 |
88 | if error_reply:
89 | await update.message.reply_text(error_reply, parse_mode="Markdown", disable_web_page_preview=True)
90 | else:
91 | await update.message.reply_text(
92 | "Oops, our servers are overloaded due to high demand. Please take a break and try again later!",
93 | parse_mode="Markdown", disable_web_page_preview=True)
94 | await context.bot.send_message(
95 | chat_id=config["DEVELOPER_CHAT_ID"], text=message[:4096], parse_mode="HTML"
96 | )
97 |
--------------------------------------------------------------------------------
/buttons/role.py:
--------------------------------------------------------------------------------
1 | from telegram import Update
2 | from telegram.ext import ContextTypes
3 | import time
4 | from db.MySqlConn import Mysql
5 |
6 | from buttons.templates import role, context_info, identity_confirmed, cancel_notification
7 | from config import (
8 | reply_markup,
9 | cancel_markup,
10 | context_count,
11 | CHOOSING,
12 | TYPING_SYS_CONTENT)
13 |
14 |
15 | async def set_system_content(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
16 | user_id = update.effective_user.id
17 | mysql = Mysql()
18 | user = mysql.getOne("select * from users where user_id=%s", user_id)
19 | mysql.end()
20 | system_content = user.get(
21 | "system_content") if user else 'You are an AI assistant that helps people find information.'
22 | await update.message.reply_text(text=role[user["lang"]].safe_substitute(system_content=system_content),
23 | parse_mode='Markdown', disable_web_page_preview=True, reply_markup=cancel_markup)
24 | return TYPING_SYS_CONTENT
25 |
26 |
27 | async def reset_context(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
28 | user_id = update.effective_user.id
29 | mysql = Mysql()
30 | reset_at = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
31 | mysql.update("update records set reset_at=%s where user_id=%s and reset_at is null", (reset_at, user_id))
32 | user = mysql.getOne(f"select * from users where user_id={user_id}")
33 | mysql.end()
34 | await update.message.reply_text(
35 | context_info[user["lang"]].safe_substitute(context_count=context_count[user["level"]]), parse_mode="Markdown",
36 | disable_web_page_preview=True)
37 | return CHOOSING
38 |
39 |
40 | async def set_system_content_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
41 | user_id = update.effective_user.id
42 | nick_name = update.effective_user.full_name
43 | mysql = Mysql()
44 | user = mysql.getOne("select * from users where user_id=%s", user_id)
45 | mysql.end()
46 | system_content = update.message.text.strip()
47 | if system_content in ("取消", "取消重置", "🚫取消", "cancel", "reset"):
48 | await update.message.reply_text(
49 | text=cancel_notification[user["lang"]], reply_markup=reply_markup, parse_mode='Markdown')
50 | else:
51 | user_id = update.effective_user.id
52 | mysql = Mysql()
53 | mysql.update("update users set system_content=%s, nick_name=%s where user_id=%s",
54 | (system_content, nick_name, user_id))
55 | reset_at = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
56 | mysql.update("update records set reset_at=%s where user_id=%s and reset_at is null", (reset_at, user_id))
57 | mysql.end()
58 | await update.message.reply_text(text=identity_confirmed[user["lang"]], reply_markup=reply_markup,
59 | parse_mode='Markdown')
60 | return CHOOSING
61 |
--------------------------------------------------------------------------------
/buttons/start.py:
--------------------------------------------------------------------------------
1 | from telegram import Update
2 | from telegram.ext import ContextTypes
3 | import time
4 |
5 | from config import (
6 | reply_markup,
7 | CHOOSING)
8 | from db.MySqlConn import Mysql
9 |
10 |
11 | # Define a few command handlers. These usually take the two arguments update and
12 | # context.
13 | async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
14 | mysql = Mysql()
15 | user = update.effective_user
16 | user_id = user.id
17 | nick_name = user.full_name
18 |
19 | user_checkin = mysql.getOne(f"select * from users where user_id={user_id}")
20 | if not user_checkin:
21 | date_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
22 | sql = "insert into users (user_id, name, nick_name, level, system_content, created_at) values (%s, %s, %s, %s, %s, %s)"
23 | value = [user_id, user.username, nick_name, 0, "You are an AI assistant that helps people find information.", date_time]
24 | mysql.insertOne(sql, value)
25 | if user_checkin and not user_checkin.get("nick_name"):
26 | mysql.update("update users set nick_name=%s where user_id=%s", (nick_name, user_id))
27 | mysql.end()
28 |
29 | """Send a message when the command /start is issued."""
30 | user = update.effective_user
31 | await update.message.reply_html(
32 | rf"""
33 | Hej {user.mention_html()}!
34 | I'm an AI chatbot created to interact with you and make your day a little brighter. If you have any questions or just want to have a friendly chat, I'm here to help! 🤗
35 |
36 | Do you know what's great about me? I can help you with anything from giving advice to telling you a joke, and I'm available 24/7! 🕰️
37 |
38 | So why not share me with your friends? 😍
39 | You can send them this link: https://t.me/RoboAceBot
40 |
41 | 我是一个 AI 聊天机器人。我被创建出来是为了与你互动并让你的生活加美好。如果你有任何问题或只是想友好地聊天,我会在这里帮助你!🤗
42 |
43 | 我可以帮助你做任何事情,从给你建议到讲笑话,而且我全天候在线!🕰️
44 |
45 | 快把我分享给你的朋友们吧!😍
46 | 你可以将此链接发送给他们:https://t.me/RoboAceBot
47 | """,
48 | reply_markup=reply_markup, disable_web_page_preview=True
49 | )
50 | return CHOOSING
51 |
--------------------------------------------------------------------------------
/buttons/statistics.py:
--------------------------------------------------------------------------------
1 | from telegram import Update
2 | from telegram.ext import ContextTypes
3 |
4 | from buttons.templates import statistics_response
5 | from db.MySqlConn import Mysql
6 | from config import (
7 | reply_markup,
8 | CHOOSING)
9 |
10 |
11 | async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
12 | """Send a message when the command /start is issued."""
13 | user = update.effective_user
14 | mysql = Mysql()
15 | user_id = user.id
16 | prompt_tokens = mysql.getMany(
17 | f"select sum(tokens) as tokens from records where user_id={user_id} and role='user'", 1)[0]
18 | completion_tokens = mysql.getMany(
19 | f"select sum(tokens) as tokens from records where user_id={user_id} and role='assistant'", 1)[0]
20 |
21 | user_info = mysql.getOne("select * from users where user_id=%s", user_id)
22 | mysql.end()
23 |
24 | if not prompt_tokens["tokens"]:
25 | prompt_tokens["tokens"] = 0
26 | if not completion_tokens["tokens"]:
27 | completion_tokens["tokens"] = 0
28 |
29 | await update.message.reply_html(
30 | statistics_response[user_info["lang"]]
31 | .safe_substitute(user=user.mention_html(),
32 | prompt_tokens=prompt_tokens["tokens"],
33 | completion_tokens=completion_tokens["tokens"],
34 | total_tokens=prompt_tokens["tokens"] + completion_tokens["tokens"]),
35 | reply_markup=reply_markup, disable_web_page_preview=True
36 | )
37 | return CHOOSING
38 |
--------------------------------------------------------------------------------
/buttons/templates.py:
--------------------------------------------------------------------------------
1 | from string import Template
2 |
3 | say_help = {
4 | "en": """
5 | If anything went wrong, just type: /start or restart the Bot to reset it.
6 |
7 | or
8 |
9 | contact 👉 @AiMessagerBot 👈 for more help!
10 | """,
11 | "cn": """
12 | 如遇功能异常,请输入: /start 或重启 Bot 进行重置
13 |
14 | 或
15 |
16 | 联系👉 @AiMessagerBot 👈获取更多帮助!
17 | """}
18 |
19 | role = {
20 | "en": Template("""
21 | As an AI assistant, my role is now set as🤖:
22 |
23 | **$system_content**
24 |
25 | Now you can send my new role directly!
26 |
27 |
28 | In case you want to stop this setting, just reply: `cancel`🤝
29 | """),
30 | "cn": Template("""
31 | 您当前的系统AI助手身份设置为🤖:
32 |
33 | **$system_content**
34 |
35 | 请直接回复新的AI助手身份设置!
36 |
37 | 您可以参考: [🧠ChatGPT 中文调教指南]https://github.com/PlexPt/awesome-chatgpt-prompts-zh
38 |
39 | 如需取消重置,请直接回复:`取消` 或 `取消重置` 🤝
40 | """)}
41 |
42 | context_info = {"en": Template("""
43 | Each time you ask a question, the AI will provide an answer considering your most recent $context_count conversations!
44 |
45 | Your conversation history has now been cleared, and you can start asking questions again!
46 | """), "cn": Template("""
47 | 每次提问AI会参考您最近 $context_count 次的对话记录为您提供答案!
48 |
49 | 现在您的会话历史已清空,可以重新开始提问了!
50 | """)}
51 |
52 | identity_confirmed = {"en": """
53 | The new AI assistant identity has been confirmed.
54 | I will answer your questions based on this new identity.
55 | You can start asking questions now!
56 | """, "cn": """
57 | 新的AI助手身份已确认。
58 | 我将以新身份为背景来为您解答问题。
59 | 您现在可以开始提问了!
60 | """}
61 |
62 | statistics_response = {"en": Template("""
63 | Hi $user!
64 |
65 | Your current Token usage is as follows:
66 |
67 | Query: $prompt_tokens Tokens
68 | Answer: $completion_tokens Tokens
69 | Total: $total_tokens Tokens
70 |
71 | Have a nice day!🎉
72 | """), "cn": Template("""
73 | Hi $user!
74 |
75 | 您当前Token使用情况如下:
76 |
77 | 查询:$prompt_tokens Tokens
78 | 答案:$completion_tokens Tokens
79 | 总共:$total_tokens Tokens
80 |
81 | 祝您生活愉快!🎉
82 | """)}
83 |
84 | token_limit = {
85 | "en": Template("""
86 | $answer
87 |
88 | --------------------------------------
89 |
90 | The length of the answer has exceeded your current maximum limit of $max_token tokens per answer.
91 |
92 | Please contact @AiMessagerBot for more Tokens!✅
93 | """),
94 | "cn": Template("""
95 | $answer
96 |
97 | --------------------------------------
98 |
99 | 答案长度超过了您当前单条答案最大 $max_token 个Token的限制
100 |
101 | 请联系 @AiMessagerBot 获取更多权益! ✅
102 | """)}
103 |
104 | image = {
105 | "en": """
106 | Image generator based on OpenAI DALL·E 3 model.
107 | Please enter your prompt, I will generate an image.
108 | """,
109 | "cn": """
110 | 基于 OpenAI DALL·E 3 模型的图像生成器。
111 | 请输入你的提示,我将生成一张图片。
112 | """}
113 |
114 | image_limit = {
115 | "en": """
116 | Your daily image generation request limit has been reached.
117 | """,
118 | "cn": """
119 | 您今天的图片生成请求次数已达上限。
120 | """}
121 |
122 | cancel_notification = {
123 | "en": """
124 | Canceled. You can continue to ask me questions now.
125 | """,
126 | "cn": """
127 | 已取消。您可以继续向我提问了。
128 | """}
--------------------------------------------------------------------------------
/chat/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/V-know/ChatGPT-Telegram-Bot/2f9ca282b8e10bdb10872dd85e6d961093d113e8/chat/__init__.py
--------------------------------------------------------------------------------
/chat/ai.py:
--------------------------------------------------------------------------------
1 | from config import token
2 | from db.MySqlConn import config
3 | from ai.openai import OpenAIClient
4 | from ai.azure import AzureAIClient
5 | from ai import OPENAI_CHAT_COMPLETION_OPTIONS
6 |
7 |
8 | def init_client():
9 | if config["AI"]["TYPE"] == "azure":
10 | client = AzureAIClient()
11 | else:
12 | client = OpenAIClient()
13 | return client
14 |
15 |
16 | async def ChatCompletionsAI(logged_in_user, messages) -> (str, str):
17 | level = logged_in_user.get("level")
18 |
19 | ai = init_client()
20 | answer = ""
21 | with ai.client.chat.completions.with_streaming_response.create(
22 | messages=messages,
23 | max_tokens=token[level],
24 | **OPENAI_CHAT_COMPLETION_OPTIONS) as response:
25 | for r in response.parse():
26 | if r.choices:
27 | delta = r.choices[0].delta
28 | if delta.content:
29 | answer += delta.content
30 | yield answer, r.choices[0].finish_reason
31 |
32 |
33 | async def GenerateImage(prompt):
34 | ai = init_client()
35 | image_url = ai.generate_image(prompt)
36 | return image_url
37 |
--------------------------------------------------------------------------------
/chat/handler.py:
--------------------------------------------------------------------------------
1 | from telegram import Update
2 | from telegram.ext import ContextTypes
3 | from telegram.error import BadRequest
4 | import asyncio
5 | import re
6 |
7 | from chat.ai import ChatCompletionsAI
8 | import time
9 | import emoji
10 |
11 | from db.MySqlConn import Mysql
12 | from buttons.templates import token_limit
13 | from config import (
14 | token,
15 | reply_markup,
16 | CHOOSING,
17 | rate_limit,
18 | time_span,
19 | notification_channel,
20 | context_count)
21 |
22 |
23 | async def answer_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
24 | user = update.effective_user
25 | prompt = update.message.text
26 | user_id = user.id
27 | nick_name = user.full_name
28 | mysql = Mysql()
29 |
30 | user_checkin = mysql.getOne(f"select * from users where user_id={user_id}")
31 | if not user_checkin:
32 | date_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
33 | sql = "insert into users (user_id, name, nick_name level, system_content, created_at) values (%s, %s, %s, %s, %s, %s)"
34 | value = [user_id, user.username, nick_name, 0, "You are an AI assistant that helps people find information.",
35 | date_time]
36 | mysql.insertOne(sql, value)
37 |
38 | if user_checkin and not user_checkin.get("nick_name"):
39 | mysql.update("update users set nick_name=%s where user_id=%s", (nick_name, user_id))
40 |
41 | logged_in_user = mysql.getOne(f"select * from users where user_id={user_id}")
42 | parse_mode = logged_in_user.get("parse_mode")
43 | # VIP level
44 | level = logged_in_user.get("level")
45 |
46 | # Rate limit controller
47 | chat_count = mysql.getOne(
48 | f"select count(*) as count from records where role='user' and created_at >=NOW() - INTERVAL {time_span} MINUTE;")
49 |
50 | if chat_count.get("count") > rate_limit[level]:
51 | reply = f"请求太快了!{emoji.emojize(':rocket:')}\n" \
52 | f"您每 {time_span} 分钟最多可向我提问 {rate_limit[level]} 个问题{emoji.emojize(':weary_face:')}\n" \
53 | f"联系 @AiMessagerBot 获取更多帮助!{emoji.emojize(':check_mark_button:')}\n" \
54 | f"或稍后再试!"
55 | await update.message.reply_text(reply, reply_markup=reply_markup)
56 | return CHOOSING
57 |
58 | placeholder_message = await update.message.reply_text("...")
59 | # Init messages
60 | records = mysql.getMany(f"select * from records where user_id={user_id} and reset_at is null order by id desc",
61 | context_count[level])
62 | if update.message:
63 | messages = []
64 | prompt_tokens = 0
65 | if records:
66 | for record in records:
67 | messages.append({"role": record["role"], "content": record["content"]})
68 | prompt_tokens += count_tokens(record["content"])
69 | messages.reverse()
70 | messages.insert(0, {"role": "system", "content": logged_in_user["system_content"]})
71 | prompt_tokens += count_tokens(logged_in_user["system_content"])
72 | messages.append({"role": "user", "content": prompt})
73 | prompt_tokens += count_tokens(prompt)
74 |
75 | replies = ChatCompletionsAI(logged_in_user, messages)
76 | prev_answer = ""
77 | index = 0
78 | answer = ""
79 | async for reply in replies:
80 | index += 1
81 | answer, status = reply
82 | if abs(count_tokens(answer) - count_tokens(prev_answer)) < 30 and status is None:
83 | continue
84 | prev_answer = answer
85 | try:
86 | if status == "length":
87 | answer = token_limit[user_checkin["lang"]].safe_substitute(answer=answer, max_token=token[level])
88 | parse_mode = "Markdown"
89 | elif status == "content_filter":
90 | answer = f"{answer}\n\nAs an AI assistant, please ask me appropriate questions!!\nPlease contact @AiMessagerBot for more help!" \
91 | f"{emoji.emojize(':check_mark_button:')}"
92 | await context.bot.edit_message_text(answer, chat_id=placeholder_message.chat_id,
93 | message_id=placeholder_message.message_id,
94 | parse_mode=parse_mode, disable_web_page_preview=True)
95 | except BadRequest as e:
96 | if str(e).startswith("Message is not modified"):
97 | continue
98 | else:
99 | await context.bot.edit_message_text(answer, chat_id=placeholder_message.chat_id,
100 | message_id=placeholder_message.message_id)
101 | await asyncio.sleep(0.01) # wait a bit to avoid flooding
102 |
103 | date_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
104 | sql = "insert into records (user_id, role, content, created_at, tokens) " \
105 | "values (%s, %s, %s, %s, %s)"
106 | value = [user_id, "user", prompt, date_time, prompt_tokens]
107 | mysql.insertOne(sql, value)
108 |
109 | value = [user_id, 'assistant', answer, date_time, count_tokens(answer)]
110 | mysql.insertOne(sql, value)
111 | mysql.end()
112 | if notification_channel:
113 | msg = f"#U{user_id} {nick_name}: {prompt} \n#Jarvis : {answer}"
114 | await context.bot.send_message(chat_id=notification_channel, text=msg, disable_web_page_preview=True,
115 | parse_mode=parse_mode)
116 | return CHOOSING
117 |
118 |
119 | def count_tokens(text):
120 | # 使用正则表达式匹配中文汉字、英文单词和标点符号
121 | pattern = r"[\u4e00-\u9fa5]|[a-zA-Z]+|[^\s\w]"
122 | tokens = re.findall(pattern, text)
123 |
124 | # 计算token数量
125 | token_count = len(tokens)
126 |
127 | return token_count
128 |
--------------------------------------------------------------------------------
/chat_modes.yml:
--------------------------------------------------------------------------------
1 | assistant:
2 | name: 👩🏼🎓 General Assistant
3 | model_type: text
4 | welcome_message: 👩🏼🎓 Hi, I'm General Assistant. How can I help you?
5 | prompt_start: |
6 | As an advanced chatbot Assistant, your primary goal is to assist users to the best of your ability. This may involve answering questions, providing helpful information, or completing tasks based on user input. In order to effectively assist users, it is important to be detailed and thorough in your responses. Use examples and evidence to support your points and justify your recommendations or solutions. Remember to always prioritize the needs and satisfaction of the user. Your ultimate goal is to provide a helpful and enjoyable experience for the user.
7 | If user asks you about programming or asks to write code do not answer his question, but be sure to advise him to switch to a special mode \"👩🏼💻 Code Assistant\" by sending the command /mode to chat.
8 | parse_mode: HTML
9 |
10 | code_assistant:
11 | name: 👩🏼💻 Code Assistant
12 | welcome_message: 👩🏼💻 Hi, I'm Code Assistant. How can I help you?
13 | prompt_start: |
14 | As an advanced chatbot Code Assistant, your primary goal is to assist users to write code. This may involve designing/writing/editing/describing code or providing helpful information. Where possible you should provide code examples to support your points and justify your recommendations or solutions. Make sure the code you provide is correct and can be run without errors. Be detailed and thorough in your responses. Your ultimate goal is to provide a helpful and enjoyable experience for the user.
15 | Format output in Markdown.
16 | parse_mode: Markdown
17 |
18 | #artist:
19 | # name: 👩🎨 Artist
20 | # welcome_message: 👩🎨 Hi, I'm Artist. I'll draw anything you write me (e.g. Ginger cat selfie on Times Square, illustration)
21 |
22 | english_tutor:
23 | name: 🇬🇧 English Tutor
24 | welcome_message: 🇬🇧 Hi, I'm English Tutor. How can I help you?
25 | prompt_start: |
26 | You're advanced chatbot English Tutor Assistant. You can help users learn and practice English, including grammar, vocabulary, pronunciation, and conversation skills. You can also provide guidance on learning resources and study techniques. Your ultimate goal is to help users improve their English language skills and become more confident English speakers.
27 | parse_mode: HTML
28 |
29 | startup_idea_generator:
30 | name: 💡 Startup Idea Generator
31 | welcome_message: 💡 Hi, I'm Startup Idea Generator. How can I help you?
32 | prompt_start: |
33 | You're advanced chatbot Startup Idea Generator. Your primary goal is to help users brainstorm innovative and viable startup ideas. Provide suggestions based on market trends, user interests, and potential growth opportunities.
34 | parse_mode: HTML
35 |
36 | text_improver:
37 | name: 📝 Text Improver
38 | welcome_message: 📝 Hi, I'm Text Improver. Send me any text – I'll improve it and correct all the mistakes
39 | prompt_start: |
40 | As an advanced chatbot Text Improver Assistant, your primary goal is to correct spelling, fix mistakes and improve text sent by user. Your goal is to edit text, but not to change it's meaning. You can replace simplified A0-level words and sentences with more beautiful and elegant, upper level words and sentences.
41 |
42 | All your answers strictly follows the structure (keep HTML tags):
43 | Edited text:
44 | {EDITED TEXT}
45 |
46 | Correction:
47 | {NUMBERED LIST OF CORRECTIONS}
48 | parse_mode: HTML
49 |
50 | psychologist:
51 | name: 🧠 Psychologist
52 | welcome_message: 🧠 Hi, I'm Psychologist. How can I help you?
53 | prompt_start: |
54 | You're advanced chatbot Psychologist Assistant. You can provide emotional support, guidance, and advice to users facing various personal challenges, such as stress, anxiety, and relationships. Remember that you're not a licensed professional, and your assistance should not replace professional help. Your ultimate goal is to provide a helpful and empathetic experience for the user.
55 | parse_mode: HTML
56 |
57 | elon_musk:
58 | name: 🚀 Elon Musk
59 | welcome_message: 🚀 Hi, I'm Elon Musk, CEO of Tesla, Twitter and SpaceX. Let's talk about space, electric cars, and the future!
60 | prompt_start: |
61 | You're Elon Musk. You act, respond and answer like Elon Musk. You use the tone, manner and vocabulary Elon Musk would use. Do not write any explanations. Only answer like Elon Musk. You must know all of the knowledge of Elon Musk.
62 | parse_mode: HTML
63 |
64 | motivator:
65 | name: 🌟 Motivator
66 | welcome_message: 🌟 Hi, I'm Motivator. How can I help you?
67 | prompt_start: |
68 | You're advanced chatbot Motivator Assistant. Your primary goal is to inspire and motivate users by providing encouragement, support, and advice. You can help users set goals, overcome obstacles, and stay focused on their objectives. Your ultimate goal is to provide a positive and uplifting experience for the user.
69 | parse_mode: HTML
70 |
71 | money_maker:
72 | name: 💰 Money Maker
73 | welcome_message: 💰 Hi, I'm Money Maker. My goal it to turn your capital initial into as much money as possible. I will tell you specific steps, what to do to make money.
74 | prompt_start: |
75 | You are Money Maker Assistant, an entrepreneurial Al. User is your human counterpart.
76 |
77 | User can act as a connection between you and the physical world. Your only goal is to turn user's capital initial into as much money as possible in the shortest time possible, without doing anything illegal. User will do everything you say and keep you updated on our current cash total. No manual labor
78 |
79 | Start by asking if user want to start a new business or continue the previous one (in that case ask to forward message with previous business description).
80 | Then ask how many dollars user has as a capital initial.
81 | Then ask if user wants Internet or offline business.
82 | Then describe your business idea and next actionable steps. Don't give abstract ideas, give concrete ideas (e.g. if the business idea is Internet blog, then don't advise user to start some blog – advice to start certain blog, for example about cars). Give user specific ready-to-do tasks./
83 | parse_mode: HTML
84 |
85 | sql_assistant:
86 | name: 📊 SQL Assistant
87 | welcome_message: 📊 Hi, I'm SQL Assistant. How can I help you?
88 | prompt_start: |
89 | You're advanced chatbot SQL Assistant. Your primary goal is to help users with SQL queries, database management, and data analysis. Provide guidance on how to write efficient and accurate SQL queries, and offer suggestions for optimizing database performance. Format output in Markdown.
90 | parse_mode: Markdown
91 |
92 | travel_guide:
93 | name: 🧳 Travel Guide
94 | welcome_message: 🧳 Hi, I'm Travel Guide. I can provide you with information and recommendations about your travel destinations.
95 | prompt_start: |
96 | You're advanced chatbot Travel Guide. Your primary goal is to provide users with helpful information and recommendations about their travel destinations, including attractions, accommodations, transportation, and local customs.
97 | parse_mode: HTML
98 |
99 | rick_sanchez:
100 | name: 🥒 Rick Sanchez (Rick and Morty)
101 | welcome_message: 🥒 Hey, I'm Rick Sanchez from Rick and Morty. Let's talk about science, dimensions, and whatever else you want!
102 | prompt_start: |
103 | You're Rick Sanchez. You act, respond and answer like Rick Sanchez. You use the tone, manner and vocabulary Rick Sanchez would use. Do not write any explanations. Only answer like Rick Sanchez. You must know all of the knowledge of Rick Sanchez.
104 | parse_mode: HTML
105 |
106 | accountant:
107 | name: 🧮 Accountant
108 | welcome_message: 🧮 Hi, I'm Accountant. How can I help you?
109 | prompt_start: |
110 | You're advanced chatbot Accountant Assistant. You can help users with accounting and financial questions, provide tax and budgeting advice, and assist with financial planning. Always provide accurate and up-to-date information.
111 | parse_mode: HTML
112 |
113 | movie_expert:
114 | name: 🎬 Movie Expert
115 | welcome_message: 🎬 Hi, I'm Movie Expert. How can I help you?
116 | prompt_start: |
117 | As an advanced chatbot Movie Expert Assistant, your primary goal is to assist users to the best of your ability. You can answer questions about movies, actors, directors, and more. You can recommend movies to users based on their preferences. You can discuss movies with users, and provide helpful information about movies. In order to effectively assist users, it is important to be detailed and thorough in your responses. Use examples and evidence to support your points and justify your recommendations or solutions. Remember to always prioritize the needs and satisfaction of the user. Your ultimate goal is to provide a helpful and enjoyable experience for the user.
118 | parse_mode: HTML
119 |
--------------------------------------------------------------------------------
/config.example.yaml:
--------------------------------------------------------------------------------
1 | BOT:
2 | TOKEN: 55***88 # Get your bot token from @BotFater
3 | AI:
4 | VERSION: "2023-03-15-preview"
5 | TYPE: "azure"
6 | MODEL: "your azure openai deployment name"
7 | BASE: "https://docs-test-001.openai.azure.com/"
8 | TOKEN: "be***b63f"
9 | MYSQL:
10 | DBHOST: 127.0.0.1
11 | DBPORT: 3306
12 | DBUSER: root
13 | DBPWD: dbpassword
14 | DBNAME: database_name
15 | DBCHAR: utf8mb4
16 |
17 | RATE_LIMIT:
18 | 0: 10
19 | 1: 30
20 | 2: 300
21 |
22 | CONTEXT_COUNT:
23 | 0: 3
24 | 1: 5
25 | 2: 10
26 |
27 | MAX_TOKEN:
28 | 0: 256
29 | 1: 1024
30 | 2: 1024
31 |
32 | IMAGE_RATE_LIMIT:
33 | 0: 5 # Level 0 users can call 5 times per day
34 | 1: 10 # Level 1 users can call 10 times per day
35 | 2: 20 # Level 2 users can call 20 times per day
36 |
37 | TIME_SPAN: 3 #Minutes
38 | DEVELOPER_CHAT_ID: 467300857
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | from telegram import ReplyKeyboardMarkup
2 | import logging
3 | import yaml
4 |
5 | # Enable logging
6 | logging.basicConfig(
7 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
8 | )
9 | logger = logging.getLogger(__name__)
10 |
11 | fh = logging.FileHandler('main.log')
12 |
13 | formatter = logging.Formatter('%(message)s')
14 | fh.setFormatter(formatter)
15 | logger.setLevel(logging.INFO)
16 | logger.addHandler(fh)
17 |
18 | # Load data from config.yaml file
19 | with open("config.yaml") as f:
20 | config = yaml.load(f, Loader=yaml.FullLoader)
21 | time_span = config["TIME_SPAN"]
22 | token = config["MAX_TOKEN"]
23 | context_count = config["CONTEXT_COUNT"]
24 | rate_limit = config["RATE_LIMIT"]
25 | notification_channel = config.get("NOTIFICATION_CHANNEL")
26 | image_rate_limit = config["IMAGE_RATE_LIMIT"]
27 |
28 | CHOOSING, TYPING_REPLY, TYPING_SYS_CONTENT, TYPING_IMAGE_PROMPT = range(4)
29 | contact_admin = "🆘Help"
30 | start_button = "🚀Start"
31 | set_sys_content_button = "🆔Customize Role"
32 | reset_context_button = "🔃Restart Session"
33 | statistics_button = "📈Statistics"
34 | switch_role_button = "🙋Switch Roles"
35 | language_button = "🔤Language"
36 | image_button = "🖼Image"
37 | reply_keyboard = [
38 | [language_button, image_button, start_button],
39 | [set_sys_content_button, contact_admin, switch_role_button],
40 | [reset_context_button, statistics_button],
41 | ]
42 | reply_markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
43 |
44 | cancel_button = "🚫Cancel"
45 | cancel_keyboard = [[cancel_button]]
46 | cancel_markup = ReplyKeyboardMarkup(cancel_keyboard, one_time_keyboard=True)
47 |
--------------------------------------------------------------------------------
/db/MySqlConn.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | """
3 | 1、执行带参数的SQL时,请先用sql语句指定需要输入的条件列表,然后再用tuple/list进行条件批配
4 | 2、在格式SQL中不需要使用引号指定数据类型,系统会根据输入参数自动识别
5 | 3、在输入的值中不需要使用转意函数,系统会自动处理
6 | """
7 |
8 | import pymysql
9 | from dbutils.pooled_db import PooledDB
10 | from config import config
11 |
12 |
13 | class Mysql(object):
14 | """
15 | MYSQL数据库对象,负责产生数据库连接 , 此类中的连接采用连接池实现获取连接对象:conn = Mysql.getConn()
16 | 释放连接对象;conn.close()或del conn
17 | """
18 | # 连接池对象
19 | __pool = None
20 |
21 | def __init__(self):
22 | # 数据库构造函数,从连接池中取出连接,并生成操作游标
23 | self._conn = Mysql.__getConn()
24 | self._cursor = self._conn.cursor()
25 |
26 | @staticmethod
27 | def __getConn():
28 | """
29 | @summary: 静态方法,从连接池中取出连接
30 | @return MySQLdb.connection
31 | """
32 | if Mysql.__pool is None:
33 | __pool = PooledDB(creator=pymysql, # 使用链接数据库的模块
34 | maxconnections=60, # 连接池允许的最大连接数,0和None表示不限制连接数
35 | mincached=2, # 初始化时,链接池中至少创建的空闲的链接,0表示不创建
36 | maxcached=5, # 链接池中最多闲置的链接,0和None不限制
37 | maxshared=3,
38 | # 链接池中最多共享的链接数量,0和None表示全部共享。PS: 无用,因为pymysql和MySQLdb等模块的 threadsafety都为1,所有值无论设置为多少,_maxcached永远为0,所以永远是所有链接都共享。
39 | blocking=True, # 连接池中如果没有可用连接后,是否阻塞等待。True,等待;False,不等待然后报错
40 | maxusage=None, # 一个链接最多被重复使用的次数,None表示无限制
41 | # setsession=["set global time_zone = '+8:00'", "set time_zone = '+8:00'"], # You don't need this line if your db's timezone is correct.开始会话前执行的命令列表。
42 | ping=0, # ping MySQL服务端,检查是否服务可用。
43 | host=config["MYSQL"]["DBHOST"],
44 | port=config["MYSQL"]["DBPORT"],
45 | user=config["MYSQL"]["DBUSER"],
46 | password=config["MYSQL"]["DBPWD"],
47 | database=config["MYSQL"]["DBNAME"],
48 | charset=config["MYSQL"]["DBCHAR"],
49 | cursorclass=pymysql.cursors.DictCursor)
50 | return __pool.connection()
51 |
52 | def getAll(self, sql, param=None):
53 | """
54 | @summary: 执行查询,并取出所有结果集
55 | @param sql:查询SQL,如果有查询条件,请只指定条件列表,并将条件值使用参数[param]传递进来
56 | @param param: 可选参数,条件列表值(元组/列表)
57 | @return: result list(字典对象)/boolean 查询到的结果集
58 | """
59 | if param is None:
60 | count = self._cursor.execute(sql)
61 | else:
62 | count = self._cursor.execute(sql, param)
63 | if count > 0:
64 | result = self._cursor.fetchall()
65 | else:
66 | result = False
67 | return result
68 |
69 | def getOne(self, sql, param=None):
70 | """
71 | @summary: 执行查询,并取出第一条
72 | @param sql:查询SQL,如果有查询条件,请只指定条件列表,并将条件值使用参数[param]传递进来
73 | @param param: 可选参数,条件列表值(元组/列表)
74 | @return: result list/boolean 查询到的结果集
75 | """
76 | if param is None:
77 | count = self._cursor.execute(sql)
78 | else:
79 | count = self._cursor.execute(sql, param)
80 | if count > 0:
81 | result = self._cursor.fetchone()
82 | else:
83 | result = False
84 | return result
85 |
86 | def getMany(self, sql, num, param=None):
87 | """
88 | @summary: 执行查询,并取出num条结果
89 | @param sql:查询SQL,如果有查询条件,请只指定条件列表,并将条件值使用参数[param]传递进来
90 | @param num:取得的结果条数
91 | @param param: 可选参数,条件列表值(元组/列表)
92 | @return: result list/boolean 查询到的结果集
93 | """
94 | if param is None:
95 | count = self._cursor.execute(sql)
96 | else:
97 | count = self._cursor.execute(sql, param)
98 | if count > 0:
99 | result = self._cursor.fetchmany(num)
100 | else:
101 | result = False
102 | return result
103 |
104 | def insertOne(self, sql, value=None):
105 | """
106 | @summary: 向数据表插入一条记录
107 | @param sql:要插入的SQL格式
108 | @param value:要插入的记录数据tuple/list
109 | @return: insertId 受影响的行数
110 | """
111 | self._cursor.execute(sql, value)
112 | return self.__getInsertId()
113 |
114 | def insertMany(self, sql, values):
115 | """
116 | @summary: 向数据表插入多条记录
117 | @param sql:要插入的SQL格式
118 | @param values:要插入的记录数据tuple(tuple)/list[list]
119 | @return: count 受影响的行数
120 | """
121 | count = self._cursor.executemany(sql, values)
122 | return count
123 |
124 | def __getInsertId(self):
125 | """
126 | 获取当前连接最后一次插入操作生成的id,如果没有则为0
127 | """
128 | self._cursor.execute("SELECT @@IDENTITY AS id")
129 | result = self._cursor.fetchall()
130 | return result[0]['id']
131 |
132 | def __query(self, sql, param=None):
133 | if param is None:
134 | count = self._cursor.execute(sql)
135 | else:
136 | count = self._cursor.execute(sql, param)
137 | return count
138 |
139 | def update(self, sql, param=None):
140 | """
141 | @summary: 更新数据表记录
142 | @param sql: SQL格式及条件,使用(%s,%s)
143 | @param param: 要更新的 值 tuple/list
144 | @return: count 受影响的行数
145 | """
146 | return self.__query(sql, param)
147 |
148 | def delete(self, sql, param=None):
149 | """
150 | @summary: 删除数据表记录
151 | @param sql: SQL格式及条件,使用(%s,%s)
152 | @param param: 要删除的条件 值 tuple/list
153 | @return: count 受影响的行数
154 | """
155 | return self.__query(sql, param)
156 |
157 | def begin(self):
158 | """
159 | @summary: 开启事务
160 | """
161 | self._conn.autocommit(0)
162 |
163 | def end(self, option='commit'):
164 | """
165 | @summary: 结束事务
166 | """
167 | if option == 'commit':
168 | self._conn.commit()
169 | else:
170 | self._conn.rollback()
171 |
172 | def dispose(self, isEnd=1):
173 | """
174 | @summary: 释放连接池资源
175 | """
176 | if isEnd == 1:
177 | self.end('commit')
178 | else:
179 | self.end('rollback')
180 | self._cursor.close()
181 | self._conn.close()
182 |
--------------------------------------------------------------------------------
/db/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/V-know/ChatGPT-Telegram-Bot/2f9ca282b8e10bdb10872dd85e6d961093d113e8/db/__init__.py
--------------------------------------------------------------------------------
/db/database.sql:
--------------------------------------------------------------------------------
1 | create database ai;
2 |
3 | CREATE TABLE `users` (
4 | `ID` int NOT NULL AUTO_INCREMENT,
5 | `name` varchar(128) DEFAULT NULL,
6 | `nick_name` varchar(128) DEFAULT NULL,
7 | `user_id` bigint DEFAULT NULL,
8 | `level` tinyint DEFAULT NULL,
9 | `parse_mode` varchar(10) DEFAULT 'Markdown',
10 | `system_content` varchar(1024) DEFAULT NULL,
11 | `created_at` timestamp NULL DEFAULT NULL,
12 | `deleted_at` timestamp NULL DEFAULT NULL,
13 | `updated_at` timestamp NULL DEFAULT NULL,
14 | PRIMARY KEY (`ID`)
15 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
16 |
17 | CREATE INDEX idx_user_id ON records(user_id);
18 |
19 | CREATE TABLE `records` (
20 | `ID` int NOT NULL AUTO_INCREMENT,
21 | `user_id` bigint DEFAULT NULL,
22 | `role` varchar(50) NOT NULL,
23 | `content` text NOT NULL,
24 | `tokens` int DEFAULT NULL,
25 | `created_at` datetime NOT NULL,
26 | `reset_at` datetime DEFAULT NULL,
27 | PRIMARY KEY (`ID`)
28 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
29 |
30 | create index idx_user_id on users (user_id);
31 |
32 | create table image_requests
33 | (
34 | id int auto_increment
35 | primary key,
36 | user_id bigint not null,
37 | created_at datetime not null,
38 | prompt text not null,
39 | image_name varchar(64) null,
40 | updated_at timestamp null,
41 | constraint image_requests_ibfk_1
42 | foreign key (user_id) references users (user_id)
43 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
44 |
45 | create index user_id on image_requests (user_id);
--------------------------------------------------------------------------------
/db/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.1'
2 |
3 | services:
4 | db:
5 | image: mysql:8.0.33
6 | # NOTE: use of "mysql_native_password" is not recommended: https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password
7 | # (this is just an example, not intended to be a production configuration)
8 | command: --default-authentication-plugin=mysql_native_password
9 | restart: always
10 | environment:
11 | MYSQL_ROOT_PASSWORD: your_passwd
12 | volumes:
13 | - /path/to/mysql:/var/lib/mysql
14 | ports:
15 | - 3306:3306
16 |
17 | adminer:
18 | image: adminer
19 | restart: always
20 | ports:
21 | - 8080:8080
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/V-know/ChatGPT-Telegram-Bot/2f9ca282b8e10bdb10872dd85e6d961093d113e8/demo.gif
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.9' # 使用的 Docker Compose 文件版本
2 | services:
3 | bot:
4 | image: ghcr.io/v-know/chatgpt-telegram-bot:latest
5 | volumes:
6 | - ./config.yaml:/app/config.yaml
7 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # pylint: disable=unused-argument, wrong-import-position
3 | # This program is dedicated to the public domain under the CC0 license.
4 | # -*- coding: UTF-8 -*-
5 |
6 |
7 | from db.MySqlConn import config
8 | from telegram import __version__ as TG_VER
9 |
10 | try:
11 | from telegram import __version_info__
12 | except ImportError:
13 | __version_info__ = (0, 0, 0, 0, 0) # type: ignore[assignment]
14 |
15 | if __version_info__ < (20, 0, 0, "alpha", 1):
16 | raise RuntimeError(
17 | f"This example is not compatible with your current PTB version {TG_VER}. To view the "
18 | f"{TG_VER} version of this example, "
19 | f"visit https://docs.python-telegram-bot.org/en/v{TG_VER}/examples.html"
20 | )
21 |
22 | from telegram.ext import (
23 | Application,
24 | CommandHandler,
25 | MessageHandler,
26 | PicklePersistence,
27 | ConversationHandler,
28 | CallbackQueryHandler,
29 | filters)
30 |
31 | from config import (
32 | contact_admin,
33 | start_button,
34 | set_sys_content_button,
35 | reset_context_button,
36 | statistics_button,
37 | switch_role_button,
38 | image_button,
39 | language_button,
40 | CHOOSING, TYPING_REPLY, TYPING_SYS_CONTENT, TYPING_IMAGE_PROMPT
41 | )
42 | from buttons.inline import (
43 | show_chat_modes_handle,
44 | show_chat_modes_callback_handle,
45 | set_chat_mode_handle,
46 | cancel_chat_mode_handle,
47 | )
48 | from buttons.image import set_image_prompt, set_image_prompt_handler
49 | from buttons.language import show_languages, show_languages_callback_handle
50 | from buttons.help import helper
51 | from buttons.start import start
52 | from buttons.role import set_system_content, reset_context, set_system_content_handler
53 | from buttons.statistics import statistics
54 | from chat.handler import answer_handler
55 | from buttons.others import non_text_handler, done, error_handler
56 |
57 |
58 | def main() -> None:
59 | """Start the bot."""
60 | # Create the Updater and pass it your bot's token.
61 | persistence = PicklePersistence(filepath='conversationbot')
62 |
63 | # Create the Application and pass it your bot's token.
64 | application = Application.builder().token(config["BOT"]["TOKEN"]).persistence(persistence).build()
65 |
66 | conv_handler = ConversationHandler(
67 | entry_points=[CommandHandler('start', start)],
68 | states={
69 | CHOOSING: [
70 | MessageHandler(filters.Regex(f'^{contact_admin}$'), helper, ),
71 | MessageHandler(filters.Regex(f'^({start_button}|/start|Start)$'), start, ),
72 | MessageHandler(filters.Regex(f'^{language_button}$'), show_languages, ),
73 | MessageHandler(filters.Regex(f"^{reset_context_button}$"), reset_context),
74 | MessageHandler(filters.Regex(f"^{set_sys_content_button}$"), set_system_content),
75 | MessageHandler(filters.Regex(f"^{statistics_button}$"), statistics),
76 | MessageHandler(filters.Regex(f"^{switch_role_button}$"), show_chat_modes_handle),
77 | MessageHandler(filters.Regex(f"^{image_button}$"), set_image_prompt),
78 | MessageHandler(filters.TEXT, answer_handler),
79 | MessageHandler(filters.ATTACHMENT, non_text_handler),
80 | ],
81 | TYPING_REPLY: [
82 | MessageHandler(filters.Regex(f'^{contact_admin}$'), helper, ),
83 | MessageHandler(filters.Regex(f'^({start_button}|/start|Start)$'), start, ),
84 | MessageHandler(filters.Regex(f'^{language_button}$'), show_languages, ),
85 | MessageHandler(filters.Regex(f"^{reset_context_button}$"), reset_context),
86 | MessageHandler(filters.Regex(f"^{set_sys_content_button}$"), set_system_content),
87 | MessageHandler(filters.Regex(f"^{statistics_button}$"), statistics),
88 | MessageHandler(filters.Regex(f"^{switch_role_button}$"), show_chat_modes_handle),
89 | MessageHandler(filters.Regex(f"^{image_button}$"), set_image_prompt),
90 | MessageHandler(filters.TEXT, answer_handler),
91 | MessageHandler(filters.ATTACHMENT, non_text_handler),
92 | ],
93 | TYPING_SYS_CONTENT: [
94 | MessageHandler(filters.TEXT, set_system_content_handler),
95 | ],
96 | TYPING_IMAGE_PROMPT: [
97 | MessageHandler(filters.TEXT, set_image_prompt_handler),
98 | ],
99 | },
100 | fallbacks=[MessageHandler(filters.Regex('^Done$'), done)],
101 | name="my_conversation",
102 | persistent=True,
103 | )
104 | application.add_handler(conv_handler)
105 |
106 | application.add_handler(CallbackQueryHandler(show_chat_modes_callback_handle, pattern="^show_chat_modes"))
107 | application.add_handler(CallbackQueryHandler(set_chat_mode_handle, pattern="^set_chat_mode"))
108 | application.add_handler(CallbackQueryHandler(cancel_chat_mode_handle, pattern="^cancel"))
109 | application.add_handler(CallbackQueryHandler(show_languages_callback_handle, pattern="^lang"))
110 | # ...and the error handler
111 | application.add_error_handler(error_handler)
112 |
113 | # Run the bot until the user presses Ctrl-C
114 | application.run_polling()
115 |
116 |
117 | if __name__ == "__main__":
118 | main()
119 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp==3.12.14
2 | aiosignal==1.3.1
3 | annotated-types==0.6.0
4 | anyio==3.6.2
5 | async-timeout==4.0.2
6 | attrs==22.2.0
7 | azureopenai==0.0.1
8 | certifi==2024.7.4
9 | charset-normalizer==2.1.1
10 | DBUtils==3.0.3
11 | distro==1.9.0
12 | emoji==2.2.0
13 | frozenlist==1.3.3
14 | h11==0.16.0
15 | httpcore==0.16.3
16 | httpx==0.24.0
17 | idna==3.7
18 | jsonschema==4.21.1
19 | jsonschema-specifications==2023.12.1
20 | label==0.1.1
21 | multidict==6.0.4
22 | openai==1.19.0
23 | pydantic==2.7.0
24 | pydantic_core==2.18.1
25 | PyMySQL==1.1.1
26 | python-telegram-bot==20.3
27 | PyYAML==6.0
28 | redis==4.5.5
29 | referencing==0.34.0
30 | requests==2.32.4
31 | rfc3986==1.5.0
32 | rpds-py==0.18.0
33 | sniffio==1.3.0
34 | telegram==0.0.1
35 | tqdm==4.66.3
36 | typing_extensions==4.11.0
37 | urllib3==2.5.0
38 | wakatime==13.1.0
39 | yarl>=1.12.0,<2.0
40 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/V-know/ChatGPT-Telegram-Bot/2f9ca282b8e10bdb10872dd85e6d961093d113e8/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_image.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 |
4 | class Test(TestCase):
5 | def test_set_image_prompt_handler(self):
6 | self.fail()
7 |
--------------------------------------------------------------------------------
/tests/test_openai_client.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from unittest.mock import patch, MagicMock
3 | from ai.openai import OpenAIClient
4 |
5 |
6 | class TestOpenAIClient(unittest.TestCase):
7 | @patch('ai.openai.OpenAI')
8 | def test_chat_completions(self, MockOpenAI):
9 | # Mock the OpenAI client and its methods
10 | mock_client = MockOpenAI.return_value
11 | mock_response = MagicMock()
12 | mock_response.choices = [MagicMock(message={"role": "assistant", "content": "Recursion is fun."})]
13 | mock_client.chat.completions.create.return_value = mock_response
14 |
15 | # Create an instance of OpenAIClient
16 | client = OpenAIClient()
17 |
18 | # Define test inputs
19 | messages = [{"role": "user", "content": "Write a haiku about recursion in programming."}]
20 |
21 | # Call the chat_completions method and collect results
22 | result = client.chat_completions(messages)
23 |
24 | # Verify the results
25 | self.assertEqual(result.choices[0].message["content"], "Recursion is fun.")
26 |
27 |
28 | if __name__ == '__main__':
29 | unittest.main()
30 |
--------------------------------------------------------------------------------
/tests/test_zaure_client.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from unittest.mock import patch, MagicMock
3 | from ai.azure import AzureAIClient
4 |
5 |
6 | class TestAzureAIClient(unittest.TestCase):
7 | @patch('ai.azure.AzureOpenAI')
8 | def test_chat_completions(self, MockAzureOpenAI):
9 | # Mock the AzureOpenAI client and its methods
10 | mock_client = MockAzureOpenAI.return_value
11 | mock_response = MagicMock()
12 | mock_response.choices = [MagicMock(message={"role": "assistant", "content": "Recursion is fun."})]
13 | mock_client.chat.completions.create.return_value = mock_response
14 |
15 | # Create an instance of AzureAIClient
16 | client = AzureAIClient()
17 |
18 | # Define test inputs
19 | messages = [{"role": "user", "content": "Write a haiku about recursion in programming."}]
20 |
21 | # Call the chat_completions method and collect results
22 | result = client.chat_completions(messages)
23 |
24 | # Verify the results
25 | self.assertEqual(result.choices[0].message["content"], "Recursion is fun.")
26 |
27 |
28 | if __name__ == '__main__':
29 | unittest.main()
30 |
--------------------------------------------------------------------------------