├── .env.example ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.en.md ├── README.md ├── demo └── chatgpt-line-bot.gif ├── docker-compose.yaml ├── main.py ├── requirements.txt └── src ├── __init__.py ├── __pycache__ ├── __init__.cpython-39.pyc ├── logger.cpython-39.pyc ├── memory.cpython-39.pyc ├── models.cpython-39.pyc ├── mongodb.cpython-39.pyc └── storage.cpython-39.pyc ├── logger.py ├── memory.py ├── models.py ├── mongodb.py ├── service ├── __init__.py ├── website.py └── youtube.py ├── storage.py └── utils.py /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_MODEL_ENGINE = 'gpt-3.5-turbo' 2 | SYSTEM_MESSAGE = 'You are a helpful assistant.' 3 | LINE_CHANNEL_SECRET = 4 | LINE_CHANNEL_ACCESS_TOKEN = -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | .DS_Store 3 | */.DS_Store 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | 121 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-alpine 2 | 3 | 4 | COPY ./ /ChatGPT-Line-Bot 5 | WORKDIR /ChatGPT-Line-Bot 6 | 7 | RUN pip3 install -r requirements.txt 8 | 9 | CMD ["python3", "main.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ExplainThis 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. -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Line Bot 2 | 3 | [中文](README.md) | English 4 | 5 | [![license](https://img.shields.io/pypi/l/ansicolortags.svg)](LICENSE) [![Release](https://img.shields.io/github/v/release/TheExplainthis/ChatGPT-Line-Bot)](https://github.com/TheExplainthis/ChatGPT-Line-Bot/releases/) 6 | 7 | 8 | ## Update 9 | - 2023/03/23 Update summary of Youtube videos and news articles (supports: United Daily News, SET, Yahoo News, Central News Agency, Storm Media, TVBS, Liberty Times, ETtoday, China Times, Line News, TTV News) 10 | - 2023/03/18 Added Whisper service, users can now add their own tokens, and added command (refer to the documentation below) 11 | - 2023/03/03 Model change to chat completion: `gpt-3.5-turbo` 12 | 13 | 14 | ## Introduction 15 | Import the ChatGPT bot to Line and start interacting with it by simply typing text in the input box. In addition to ChatGPT, the model for DALL·E 2 is also integrated. Enter `/imagine + text` to return the corresponding image, as shown in the figure below: 16 | 17 | ![Demo](https://github.com/TheExplainthis/ChatGPT-Line-Bot/blob/main/demo/chatgpt-line-bot.gif) 18 | 19 | ## Installation Steps 20 | ### Token Retrieval 21 | 1. Retrieve the OpenAI API Token: 22 | 1. Register/login to your [OpenAI](https://beta.openai.com/) account. 23 | 2. Click on the avatar on the top right corner and select `View API keys`. 24 | 3. Click on `Create new secret key` in the middle, and the generated token will be `OPENAI_API` (to be used later). 25 | - Note: Each API has a free quota and restrictions. For details, please refer to [OpenAI Pricing](https://openai.com/api/pricing/). 26 | 2. Retrieve the Line Token: 27 | 1. Login to [Line Developer](https://developers.line.biz/zh-hant/). 28 | 2. Create a bot: 29 | 1. Create a `Provider` -> click `Create`. 30 | 2. Create a `Channel` -> select `Create a Messaging API channel`. 31 | 3. Enter the required basic information. 32 | 4. After completion, there is a `Channel Secret` under `Basic Settings` -> click `Issue`, and the generated token will be `LINE_CHANNEL_SECRET` (to be used later). 33 | 5. Under `Messaging API`, there is a `Channel access token` -> click `Issue`, and the generated token will be `LINE_CHANNEL_ACCESS_TOKEN` (to be used later). 34 | 35 | ### Project Setup 36 | 1. Fork the Github project: 37 | 1. Register/login to [GitHub](https://github.com/). 38 | 2. Go to [ChatGPT-Line-Bot](https://github.com/TheExplainthis/ChatGPT-Line-Bot). 39 | 3. Click `Star` to support the developer. 40 | 4. Click `Fork` to copy all the code to your own repository. 41 | 2. Deploy (free space): 42 | 1. Go to [replit](https://replit.com/). 43 | 2. Click `Sign Up` and log in with your `Github` account and authorize it -> click `Skip` to skip the initialization settings. 44 | 3. On the main page in the middle, click `Create` -> a pop-up window will appear, click `Import from Github` on the upper right corner. 45 | 4. If you have not added the Github repository, click the link `Connect GitHub to import your private repos.` -> check `Only select repositories` -> select `ChatGPT-Line-Bot`. 46 | 5. Go back to step 4. At this point, the `Github URL` can select the `ChatGPT-Line-Bot` project -> click `Import from Github`. 47 | 48 | ### Project Execution 49 | 1. Environment variables setting: 50 | 1. After completing the previous step of `Import`, click on `Tools` at the bottom left of the project management page in `Replit`, then click on `Secrets`. 51 | 2. Click on `Got it` on the right side to add environment variables, which includes: 52 | 1. Desired model: 53 | - key: `OPENAI_MODEL_ENGINE` 54 | - value: `gpt-3.5-turbo` 55 | 2. ChatGPT wants the assistant to play the role of a keyword (currently, no further usage instructions have been officially released, and players can test it themselves). 56 | - key: `SYSTEM_MESSAGE` 57 | - value: `You are a helpful assistant.` 58 | 3. Line Channel Secret: 59 | - key: `LINE_CHANNEL_SECRET` 60 | - value: `[obtained from step one]` 61 | 4. Line Channel Access Token: 62 | - key: `LINE_CHANNEL_ACCESS_TOKEN` 63 | - value: `[obtained from step one]` 64 | 2. Start running: 65 | 1. Click on `Run` on the top. 66 | 2. After successful, the right-side screen will display `Hello World`, and the **URL** on the top of the screen should be copied down. 67 | 3. Go back to Line Developer, paste the **URL** above `Webhook URL` under `Messaging API`, and add `/callback` to the end, for example: `https://ChatGPT-Line-Bot.explainthis.repl.co/callback` 68 | 4. Turn on `Use webhook` below. 69 | 5. Turn off `Auto-reply messages` below. 70 | - Note: if there is no request within an hour, the program will be interrupted, so the following steps are needed. 71 | 3. CronJob scheduled request sending: 72 | 1. Register/Login to [cron-job.org](https://cron-job.org/en/) 73 | 2. In the upper right corner of the panel, select `CREATE CRONJOB` 74 | 3. Enter `ChatGPT-Line-Bot` in the Title field, and enter the URL from the previous step, for example: `https://ChatGPT-Line-Bot.explainthis.repl.co/` 75 | 4. Send a request every `5 minutes` below 76 | 5. Click on `CREATE` 77 | 78 | ## Commands 79 | To start a conversation with ChatGPT, simply type your message in the text input box. Other available commands include: 80 | 81 | 82 | | Command | Description | 83 | | ------- | ----------- | 84 | | `/註冊` | Enter `/註冊` + OpenAI API Token in the input box to register your token| 85 | | `/系統訊息` | Enter `/系統訊息` + the role you want ChatGPT to play in the input box| 86 | | `/清除` | Enter `/清除` in the input box to clear the chat history| 87 | | `/圖像` | Enter `/圖像` + command in the input box to call the DALL·E 2 model and generate an image| 88 | | Voice input | Use voice input, the system will automatically translate the voice into text, and ChatGPT will respond in text| 89 | | Text input | Directly input text to enter the normal ChatGPT conversation mode| 90 | 91 | 92 | ## Support Us 93 | Like this free project? Please consider [supporting us](https://www.buymeacoffee.com/explainthis) to keep it running. 94 | 95 | [Buy Me A Coffee](https://www.buymeacoffee.com/explainthis) 96 | 97 | ## Related Projects 98 | - [gpt-ai-assistant](https://github.com/memochou1993/gpt-ai-assistant) 99 | - [ChatGPT-Discord-Bot](https://github.com/TheExplainthis/ChatGPT-Discord-Bot) 100 | 101 | ## License 102 | [MIT](LICENSE) 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Line Bot 2 | 3 | 中文 | [English](README.en.md) 4 | 5 | [![license](https://img.shields.io/pypi/l/ansicolortags.svg)](LICENSE) [![Release](https://img.shields.io/github/v/release/TheExplainthis/ChatGPT-Line-Bot)](https://github.com/TheExplainthis/ChatGPT-Line-Bot/releases/) 6 | 7 | 8 | ## 更新 9 | - 2023/03/23 更新總結 Youtube 影片內容、新聞文章(支援:聯合報、Yahoo 新聞、三立新聞網、中央通訊社、風傳媒、TVBS、自由時報、ETtoday、中時新聞網、Line 新聞、台視新聞網) 10 | - 2023/03/18 新增 Whipser 服務、用戶可以新增自己的 Token、新增指令(參考文件下方) 11 | - 2023/03/03 模型換成 chat completion: `gpt-3.5-turbo` 12 | 13 | 14 | ## 介紹 15 | 在 Line 中去導入 ChatGPT Bot,只要在輸入框直接輸入文字,即可與 ChatGPT 開始互動,除了 ChatGPT 以外,也直接串上了 DALL·E 2 的模型,輸入 `/imagine + 文字`,就會回傳相對應的圖片,如下圖所示: 16 | 17 | ![Demo](https://github.com/TheExplainthis/ChatGPT-Line-Bot/blob/main/demo/chatgpt-line-bot.gif) 18 | 19 | ## 安裝步驟 20 | ### Token 取得 21 | 1. 取得 OpenAI 給的 API Token: 22 | 1. [OpenAI](https://beta.openai.com/) 平台中註冊/登入帳號 23 | 2. 右上方有一個頭像,點入後選擇 `View API keys` 24 | 3. 點選中間的 `Create new secret key` -> 生成後即為 `OPENAI_API` (稍晚會用到) 25 | - 注意:每隻 API 有免費額度,也有其限制,詳情請看 [OpenAI Pricing](https://openai.com/api/pricing/) 26 | 2. 取得 Line Token: 27 | 1. 登入 [Line Developer](https://developers.line.biz/zh-hant/) 28 | 2. 創建機器人: 29 | 1. 創建 `Provider` -> 按下 `Create` 30 | 2. 創建 `Channel` -> 選擇 `Create a Messaging API channel` 31 | 3. 輸入完必填的基本資料 32 | 4. 輸入完成後,在 `Basic Settings` 下方,有一個 `Channel Secret` -> 按下 `Issue`,生成後即為 `LINE_CHANNEL_SECRET` (稍晚會用到) 33 | 5. 在 `Messaging API` 下方,有一個 `Channel access token` -> 按下 `Issue`,生成後即為 `LINE_CHANNEL_ACCESS_TOKEN` (稍晚會用到) 34 | 35 | ### 專案設置 36 | 1. Fork Github 專案: 37 | 1. 註冊/登入 [GitHub](https://github.com/) 38 | 2. 進入 [ChatGPT-Line-Bot](https://github.com/TheExplainthis/ChatGPT-Line-Bot) 39 | 3. 點選 `Star` 支持開發者 40 | 4. 點選 `Fork` 複製全部的程式碼到自己的倉庫 41 | 2. 部署(免費空間): 42 | 1. 進入 [replit](https://replit.com/) 43 | 2. 點選 `Sign Up` 直接用 `Github` 帳號登入並授權 -> 按下 `Skip` 跳過初始化設定 44 | 3. 進入後中間主頁的部分點選 `Create` -> 跳出框,點選右上角 `Import from Github` 45 | 4. 若尚未加入 Github 倉庫,則點選連結 `Connect GitHub to import your private repos.` -> 勾選 `Only select repositories` -> 選擇 `ChatGPT-Line-Bot` 46 | 5. 回到第四步,此時 `Github URL` 可以選擇 `ChatGPT-Line-Bot` 專案 -> 點擊 `Import from Github`。 47 | 48 | ### 專案執行 49 | 1. 環境變數設定 50 | 1. 接續上一步 `Import` 完成後在 `Replit` 的專案管理頁面左下方 `Tools` 點擊 `Secrets`。 51 | 2. 右方按下 `Got it` 後,即可新增環境變數,需新增: 52 | 1. 欲選擇的模型: 53 | - key: `OPENAI_MODEL_ENGINE` 54 | - value: `gpt-3.5-turbo` 55 | 2. ChatGPT 要讓助理扮演的角色詞(目前官方無釋出更多的使用方法,由玩家自行測試) 56 | - key: `SYSTEM_MESSAGE` 57 | - value: `You are a helpful assistant.` 58 | 3. Line Channel Secret: 59 | - key: `LINE_CHANNEL_SECRET` 60 | - value: `[由步驟一取得]` 61 | 4. Line Channel Access Token: 62 | - key: `LINE_CHANNEL_ACCESS_TOKEN` 63 | - value: `[由步驟一取得]` 64 | 2. 開始執行 65 | 1. 點擊上方的 `Run` 66 | 2. 成功後右邊畫面會顯示 `Hello World`,並將畫面中上方的**網址複製**下來 67 | 3. 回到 Line Developer,在 `Messaging API` 下方的 `Webhook URL` 江上方網址貼過來,並加上 `/callback` 例如:`https://ChatGPT-Line-Bot.explainthis.repl.co/callback` 68 | 4. 打開下方的 `Use webhook` 69 | 5. 將下方 `Auto-reply messages` 關閉 70 | - 注意:若一小時內沒有任何請求,則程式會中斷,因此需要下步驟 71 | 3. CronJob 定時發送請求 72 | 1. 註冊/登入 [cron-job.org](https://cron-job.org/en/) 73 | 2. 進入後面板右上方選擇 `CREATE CRONJOB` 74 | 3. `Title` 輸入 `ChatGPT-Line-Bot`,網址輸入上一步驟的網址,例如:`https://ChatGPT-Line-Bot.explainthis.repl.co/` 75 | 4. 下方則每 `5 分鐘` 打一次 76 | 5. 按下 `CREATE` 77 | 78 | ## 指令 79 | 在文字輸入框中直接輸入文字,即可與 ChatGPT 開始對話,而其他指令如下: 80 | 81 | | 指令 | 說明 | 82 | | --- | ----- | 83 | | `/註冊` | 在輸入框輸入 `/註冊 ` + OpenAI API Token,就可以註冊 Token| 84 | | `/系統訊息` | 在輸入框輸入 `/系統訊息 ` + 可以設定希望 ChatGPT 扮演什麼角色| 85 | | `/清除` | 在輸入框輸入 `/清除 `,就可以清除歷史訊息| 86 | | `/圖像` | 在輸入框輸入 `/圖像` + 指令,就會調用 DALL·E 2 模型,即可生成圖像。| 87 | | 語音輸入 | 利用語音輸入,系統會自動將語音翻譯成文字,並且 ChatGPT 以文字回應| 88 | | 其他文字輸入 | 直接輸入文字,則會進入一般的 ChatGPT 對話模式| 89 | 90 | 91 | ## 支持我們 92 | 如果你喜歡這個專案,願意[支持我們](https://www.buymeacoffee.com/explainthis),可以請我們喝一杯咖啡,這會成為我們繼續前進的動力! 93 | 94 | [Buy Me A Coffee](https://www.buymeacoffee.com/explainthis) 95 | 96 | ## 相關專案 97 | - [gpt-ai-assistant](https://github.com/memochou1993/gpt-ai-assistant) 98 | - [ChatGPT-Discord-Bot](https://github.com/TheExplainthis/ChatGPT-Discord-Bot) 99 | 100 | ## 授權 101 | [MIT](LICENSE) 102 | -------------------------------------------------------------------------------- /demo/chatgpt-line-bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheExplainthis/ChatGPT-Line-Bot/495d4b4af33a8e21f39b77c0cca24afa5660707c/demo/chatgpt-line-bot.gif -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | app: 5 | container_name: ChatGPT-Line-Bot 6 | build: . 7 | restart: always 8 | ports: 9 | - "${APP_PORT}:${APP_PORT}" -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from flask import Flask, request, abort 3 | from linebot import ( 4 | LineBotApi, WebhookHandler 5 | ) 6 | from linebot.exceptions import ( 7 | InvalidSignatureError 8 | ) 9 | from linebot.models import ( 10 | MessageEvent, TextMessage, TextSendMessage, ImageSendMessage, AudioMessage 11 | ) 12 | import os 13 | import uuid 14 | 15 | from src.models import OpenAIModel 16 | from src.memory import Memory 17 | from src.logger import logger 18 | from src.storage import Storage, FileStorage, MongoStorage 19 | from src.utils import get_role_and_content 20 | from src.service.youtube import Youtube, YoutubeTranscriptReader 21 | from src.service.website import Website, WebsiteReader 22 | from src.mongodb import mongodb 23 | 24 | load_dotenv('.env') 25 | 26 | app = Flask(__name__) 27 | line_bot_api = LineBotApi(os.getenv('LINE_CHANNEL_ACCESS_TOKEN')) 28 | handler = WebhookHandler(os.getenv('LINE_CHANNEL_SECRET')) 29 | storage = None 30 | youtube = Youtube(step=4) 31 | website = Website() 32 | 33 | 34 | memory = Memory(system_message=os.getenv('SYSTEM_MESSAGE'), memory_message_count=2) 35 | model_management = {} 36 | api_keys = {} 37 | 38 | 39 | @app.route("/callback", methods=['POST']) 40 | def callback(): 41 | signature = request.headers['X-Line-Signature'] 42 | body = request.get_data(as_text=True) 43 | app.logger.info("Request body: " + body) 44 | try: 45 | handler.handle(body, signature) 46 | except InvalidSignatureError: 47 | print("Invalid signature. Please check your channel access token/channel secret.") 48 | abort(400) 49 | return 'OK' 50 | 51 | 52 | @handler.add(MessageEvent, message=TextMessage) 53 | def handle_text_message(event): 54 | user_id = event.source.user_id 55 | text = event.message.text.strip() 56 | logger.info(f'{user_id}: {text}') 57 | 58 | try: 59 | if text.startswith('/註冊'): 60 | api_key = text[3:].strip() 61 | model = OpenAIModel(api_key=api_key) 62 | is_successful, _, _ = model.check_token_valid() 63 | if not is_successful: 64 | raise ValueError('Invalid API token') 65 | model_management[user_id] = model 66 | storage.save({ 67 | user_id: api_key 68 | }) 69 | msg = TextSendMessage(text='Token 有效,註冊成功') 70 | 71 | elif text.startswith('/指令說明'): 72 | msg = TextSendMessage(text="指令:\n/註冊 + API Token\n👉 API Token 請先到 https://platform.openai.com/ 註冊登入後取得\n\n/系統訊息 + Prompt\n👉 Prompt 可以命令機器人扮演某個角色,例如:請你扮演擅長做總結的人\n\n/清除\n👉 當前每一次都會紀錄最後兩筆歷史紀錄,這個指令能夠清除歷史訊息\n\n/圖像 + Prompt\n👉 會調用 DALL∙E 2 Model,以文字生成圖像\n\n語音輸入\n👉 會調用 Whisper 模型,先將語音轉換成文字,再調用 ChatGPT 以文字回覆\n\n其他文字輸入\n👉 調用 ChatGPT 以文字回覆") 73 | 74 | elif text.startswith('/系統訊息'): 75 | memory.change_system_message(user_id, text[5:].strip()) 76 | msg = TextSendMessage(text='輸入成功') 77 | 78 | elif text.startswith('/清除'): 79 | memory.remove(user_id) 80 | msg = TextSendMessage(text='歷史訊息清除成功') 81 | 82 | elif text.startswith('/圖像'): 83 | prompt = text[3:].strip() 84 | memory.append(user_id, 'user', prompt) 85 | is_successful, response, error_message = model_management[user_id].image_generations(prompt) 86 | if not is_successful: 87 | raise Exception(error_message) 88 | url = response['data'][0]['url'] 89 | msg = ImageSendMessage( 90 | original_content_url=url, 91 | preview_image_url=url 92 | ) 93 | memory.append(user_id, 'assistant', url) 94 | 95 | else: 96 | user_model = model_management[user_id] 97 | memory.append(user_id, 'user', text) 98 | url = website.get_url_from_text(text) 99 | if url: 100 | if youtube.retrieve_video_id(text): 101 | is_successful, chunks, error_message = youtube.get_transcript_chunks(youtube.retrieve_video_id(text)) 102 | if not is_successful: 103 | raise Exception(error_message) 104 | youtube_transcript_reader = YoutubeTranscriptReader(user_model, os.getenv('OPENAI_MODEL_ENGINE')) 105 | is_successful, response, error_message = youtube_transcript_reader.summarize(chunks) 106 | if not is_successful: 107 | raise Exception(error_message) 108 | role, response = get_role_and_content(response) 109 | msg = TextSendMessage(text=response) 110 | else: 111 | chunks = website.get_content_from_url(url) 112 | if len(chunks) == 0: 113 | raise Exception('無法撈取此網站文字') 114 | website_reader = WebsiteReader(user_model, os.getenv('OPENAI_MODEL_ENGINE')) 115 | is_successful, response, error_message = website_reader.summarize(chunks) 116 | if not is_successful: 117 | raise Exception(error_message) 118 | role, response = get_role_and_content(response) 119 | msg = TextSendMessage(text=response) 120 | else: 121 | is_successful, response, error_message = user_model.chat_completions(memory.get(user_id), os.getenv('OPENAI_MODEL_ENGINE')) 122 | if not is_successful: 123 | raise Exception(error_message) 124 | role, response = get_role_and_content(response) 125 | msg = TextSendMessage(text=response) 126 | memory.append(user_id, role, response) 127 | except ValueError: 128 | msg = TextSendMessage(text='Token 無效,請重新註冊,格式為 /註冊 sk-xxxxx') 129 | except KeyError: 130 | msg = TextSendMessage(text='請先註冊 Token,格式為 /註冊 sk-xxxxx') 131 | except Exception as e: 132 | memory.remove(user_id) 133 | if str(e).startswith('Incorrect API key provided'): 134 | msg = TextSendMessage(text='OpenAI API Token 有誤,請重新註冊。') 135 | elif str(e).startswith('That model is currently overloaded with other requests.'): 136 | msg = TextSendMessage(text='已超過負荷,請稍後再試') 137 | else: 138 | msg = TextSendMessage(text=str(e)) 139 | line_bot_api.reply_message(event.reply_token, msg) 140 | 141 | 142 | @handler.add(MessageEvent, message=AudioMessage) 143 | def handle_audio_message(event): 144 | user_id = event.source.user_id 145 | audio_content = line_bot_api.get_message_content(event.message.id) 146 | input_audio_path = f'{str(uuid.uuid4())}.m4a' 147 | with open(input_audio_path, 'wb') as fd: 148 | for chunk in audio_content.iter_content(): 149 | fd.write(chunk) 150 | 151 | try: 152 | if not model_management.get(user_id): 153 | raise ValueError('Invalid API token') 154 | else: 155 | is_successful, response, error_message = model_management[user_id].audio_transcriptions(input_audio_path, 'whisper-1') 156 | if not is_successful: 157 | raise Exception(error_message) 158 | memory.append(user_id, 'user', response['text']) 159 | is_successful, response, error_message = model_management[user_id].chat_completions(memory.get(user_id), 'gpt-3.5-turbo') 160 | if not is_successful: 161 | raise Exception(error_message) 162 | role, response = get_role_and_content(response) 163 | memory.append(user_id, role, response) 164 | msg = TextSendMessage(text=response) 165 | except ValueError: 166 | msg = TextSendMessage(text='請先註冊你的 API Token,格式為 /註冊 [API TOKEN]') 167 | except KeyError: 168 | msg = TextSendMessage(text='請先註冊 Token,格式為 /註冊 sk-xxxxx') 169 | except Exception as e: 170 | memory.remove(user_id) 171 | if str(e).startswith('Incorrect API key provided'): 172 | msg = TextSendMessage(text='OpenAI API Token 有誤,請重新註冊。') 173 | else: 174 | msg = TextSendMessage(text=str(e)) 175 | os.remove(input_audio_path) 176 | line_bot_api.reply_message(event.reply_token, msg) 177 | 178 | 179 | @app.route("/", methods=['GET']) 180 | def home(): 181 | return 'Hello World' 182 | 183 | 184 | if __name__ == "__main__": 185 | if os.getenv('USE_MONGO'): 186 | mongodb.connect_to_database() 187 | storage = Storage(MongoStorage(mongodb.db)) 188 | else: 189 | storage = Storage(FileStorage('db.json')) 190 | try: 191 | data = storage.load() 192 | for user_id in data.keys(): 193 | model_management[user_id] = OpenAIModel(api_key=data[user_id]) 194 | except FileNotFoundError: 195 | pass 196 | app.run(host='0.0.0.0', port=8080) 197 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | line-bot-sdk==2.4.1 2 | python-dotenv==0.21.1 3 | Flask==2.2.2 4 | opencc-python-reimplemented==0.1.4 5 | beautifulsoup4==4.11.2 6 | youtube-transcript-api==0.5.0 7 | pymongo==4.3.3 -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheExplainthis/ChatGPT-Line-Bot/495d4b4af33a8e21f39b77c0cca24afa5660707c/src/__init__.py -------------------------------------------------------------------------------- /src/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheExplainthis/ChatGPT-Line-Bot/495d4b4af33a8e21f39b77c0cca24afa5660707c/src/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /src/__pycache__/logger.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheExplainthis/ChatGPT-Line-Bot/495d4b4af33a8e21f39b77c0cca24afa5660707c/src/__pycache__/logger.cpython-39.pyc -------------------------------------------------------------------------------- /src/__pycache__/memory.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheExplainthis/ChatGPT-Line-Bot/495d4b4af33a8e21f39b77c0cca24afa5660707c/src/__pycache__/memory.cpython-39.pyc -------------------------------------------------------------------------------- /src/__pycache__/models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheExplainthis/ChatGPT-Line-Bot/495d4b4af33a8e21f39b77c0cca24afa5660707c/src/__pycache__/models.cpython-39.pyc -------------------------------------------------------------------------------- /src/__pycache__/mongodb.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheExplainthis/ChatGPT-Line-Bot/495d4b4af33a8e21f39b77c0cca24afa5660707c/src/__pycache__/mongodb.cpython-39.pyc -------------------------------------------------------------------------------- /src/__pycache__/storage.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheExplainthis/ChatGPT-Line-Bot/495d4b4af33a8e21f39b77c0cca24afa5660707c/src/__pycache__/storage.cpython-39.pyc -------------------------------------------------------------------------------- /src/logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import logging.handlers 4 | 5 | 6 | class CustomFormatter(logging.Formatter): 7 | __LEVEL_COLORS = [ 8 | (logging.DEBUG, '\x1b[40;1m'), 9 | (logging.INFO, '\x1b[34;1m'), 10 | (logging.WARNING, '\x1b[33;1m'), 11 | (logging.ERROR, '\x1b[31m'), 12 | (logging.CRITICAL, '\x1b[41m'), 13 | ] 14 | __FORMATS = None 15 | 16 | @classmethod 17 | def get_formats(cls): 18 | if cls.__FORMATS is None: 19 | cls.__FORMATS = { 20 | level: logging.Formatter( 21 | f'\x1b[30;1m%(asctime)s\x1b[0m {color}%(levelname)-8s\x1b[0m \x1b[35m%(name)s\x1b[0m -> %(message)s', 22 | '%Y-%m-%d %H:%M:%S' 23 | ) 24 | for level, color in cls.__LEVEL_COLORS 25 | } 26 | return cls.__FORMATS 27 | 28 | def format(self, record): 29 | formatter = self.get_formats().get(record.levelno) 30 | if formatter is None: 31 | formatter = self.get_formats()[logging.DEBUG] 32 | if record.exc_info: 33 | text = formatter.formatException(record.exc_info) 34 | record.exc_text = f'\x1b[31m{text}\x1b[0m' 35 | 36 | output = formatter.format(record) 37 | record.exc_text = None 38 | return output 39 | 40 | 41 | class LoggerFactory: 42 | @staticmethod 43 | def create_logger(formatter, handlers): 44 | logger = logging.getLogger('chatgpt_logger') 45 | logger.setLevel(logging.INFO) 46 | for handler in handlers: 47 | handler.setLevel(logging.DEBUG) 48 | handler.setFormatter(formatter) 49 | logger.addHandler(handler) 50 | return logger 51 | 52 | 53 | class FileHandler(logging.FileHandler): 54 | def __init__(self, log_file): 55 | os.makedirs(os.path.dirname(log_file), exist_ok=True) 56 | super().__init__(log_file) 57 | 58 | 59 | class ConsoleHandler(logging.StreamHandler): 60 | pass 61 | 62 | 63 | formatter = CustomFormatter() 64 | file_handler = FileHandler('./logs') 65 | console_handler = ConsoleHandler() 66 | logger = LoggerFactory.create_logger(formatter, [file_handler, console_handler]) 67 | -------------------------------------------------------------------------------- /src/memory.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from collections import defaultdict 3 | 4 | 5 | class MemoryInterface: 6 | def append(self, user_id: str, message: Dict) -> None: 7 | pass 8 | 9 | def get(self, user_id: str) -> str: 10 | return "" 11 | 12 | def remove(self, user_id: str) -> None: 13 | pass 14 | 15 | 16 | class Memory(MemoryInterface): 17 | def __init__(self, system_message, memory_message_count): 18 | self.storage = defaultdict(list) 19 | self.system_messages = defaultdict(str) 20 | self.default_system_message = system_message 21 | self.memory_message_count = memory_message_count 22 | 23 | def _initialize(self, user_id: str): 24 | self.storage[user_id] = [{ 25 | 'role': 'system', 'content': self.system_messages.get(user_id) or self.default_system_message 26 | }] 27 | 28 | def _drop_message(self, user_id: str): 29 | if len(self.storage.get(user_id)) >= (self.memory_message_count + 1) * 2 + 1: 30 | return [self.storage[user_id][0]] + self.storage[user_id][-(self.memory_message_count * 2):] 31 | return self.storage.get(user_id) 32 | 33 | def change_system_message(self, user_id, system_message): 34 | self.system_messages[user_id] = system_message 35 | self.remove(user_id) 36 | 37 | def append(self, user_id: str, role: str, content: str) -> None: 38 | if self.storage[user_id] == []: 39 | self._initialize(user_id) 40 | self.storage[user_id].append({ 41 | 'role': role, 42 | 'content': content 43 | }) 44 | self._drop_message(user_id) 45 | 46 | def get(self, user_id: str) -> str: 47 | return self.storage[user_id] 48 | 49 | def remove(self, user_id: str) -> None: 50 | self.storage[user_id] = [] 51 | -------------------------------------------------------------------------------- /src/models.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | import requests 3 | 4 | 5 | class ModelInterface: 6 | def check_token_valid(self) -> bool: 7 | pass 8 | 9 | def chat_completions(self, messages: List[Dict], model_engine: str) -> str: 10 | pass 11 | 12 | def audio_transcriptions(self, file, model_engine: str) -> str: 13 | pass 14 | 15 | def image_generations(self, prompt: str) -> str: 16 | pass 17 | 18 | 19 | class OpenAIModel(ModelInterface): 20 | def __init__(self, api_key: str): 21 | self.api_key = api_key 22 | self.base_url = 'https://api.openai.com/v1' 23 | 24 | def _request(self, method, endpoint, body=None, files=None): 25 | self.headers = { 26 | 'Authorization': f'Bearer {self.api_key}' 27 | } 28 | try: 29 | if method == 'GET': 30 | r = requests.get(f'{self.base_url}{endpoint}', headers=self.headers) 31 | elif method == 'POST': 32 | if body: 33 | self.headers['Content-Type'] = 'application/json' 34 | r = requests.post(f'{self.base_url}{endpoint}', headers=self.headers, json=body, files=files) 35 | r = r.json() 36 | if r.get('error'): 37 | return False, None, r.get('error', {}).get('message') 38 | except Exception: 39 | return False, None, 'OpenAI API 系統不穩定,請稍後再試' 40 | return True, r, None 41 | 42 | def check_token_valid(self): 43 | return self._request('GET', '/models') 44 | 45 | def chat_completions(self, messages, model_engine) -> str: 46 | json_body = { 47 | 'model': model_engine, 48 | 'messages': messages 49 | } 50 | return self._request('POST', '/chat/completions', body=json_body) 51 | 52 | def audio_transcriptions(self, file_path, model_engine) -> str: 53 | files = { 54 | 'file': open(file_path, 'rb'), 55 | 'model': (None, model_engine), 56 | } 57 | return self._request('POST', '/audio/transcriptions', files=files) 58 | 59 | def image_generations(self, prompt: str) -> str: 60 | json_body = { 61 | "prompt": prompt, 62 | "n": 1, 63 | "size": "512x512" 64 | } 65 | return self._request('POST', '/images/generations', body=json_body) 66 | -------------------------------------------------------------------------------- /src/mongodb.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pymongo import MongoClient 4 | 5 | 6 | class MongoDB(): 7 | """ 8 | Environment Variables: 9 | MONGODB__PATH 10 | MONGODB__DBNAME 11 | """ 12 | client: None 13 | db: None 14 | 15 | def connect_to_database(self, mongo_path=None, db_name=None): 16 | mongo_path = mongo_path or os.getenv('MONGODB__PATH') 17 | db_name = db_name or os.getenv('MONGODB__DBNAME') 18 | self.client = MongoClient(mongo_path) 19 | assert self.client.config.command('ping')['ok'] == 1.0 20 | self.db = self.client[db_name] 21 | 22 | 23 | mongodb = MongoDB() 24 | -------------------------------------------------------------------------------- /src/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheExplainthis/ChatGPT-Line-Bot/495d4b4af33a8e21f39b77c0cca24afa5660707c/src/service/__init__.py -------------------------------------------------------------------------------- /src/service/website.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import requests 4 | from bs4 import BeautifulSoup 5 | 6 | 7 | WEBSITE_SYSTEM_MESSAGE = "你現在非常擅於做資料的整理、總結、歸納、統整,並能專注於細節、且能提出觀點" 8 | WEBSITE_MESSAGE_FORMAT = """ 9 | 針對這個連結的內容: 10 | \"\"\" 11 | {} 12 | \"\"\" 13 | 14 | 請關注幾個點: 15 | 1. 他的主題為何? 16 | 2. 他的重點為何? 17 | 3. 他獨特的觀點為何? 18 | 19 | 你需要回傳的格式是: 20 | - 主題: '...' 21 | - 重點: '...' 22 | - 獨特觀點: '...' 23 | """ 24 | 25 | 26 | class Website: 27 | def get_url_from_text(self, text: str): 28 | url_regex = re.compile(r'^https?://\S+') 29 | match = re.search(url_regex, text) 30 | if match: 31 | return match.group() 32 | else: 33 | return None 34 | 35 | def get_content_from_url(self, url: str): 36 | hotpage = requests.get(url) 37 | main = BeautifulSoup(hotpage.text, 'html.parser') 38 | chunks = [article.text.strip() for article in main.find_all('article')] 39 | if chunks == []: 40 | chunks = [article.text.strip() for article in main.find_all('div', class_='content')] 41 | return chunks 42 | 43 | 44 | class WebsiteReader: 45 | def __init__(self, model=None, model_engine=None): 46 | self.system_message = os.getenv('WEBSITE_SYSTEM_MESSAGE') or WEBSITE_SYSTEM_MESSAGE 47 | self.message_format = os.getenv('WEBSITE_MESSAGE_FORMAT') or WEBSITE_MESSAGE_FORMAT 48 | self.model = model 49 | self.text_length_limit = 1800 50 | self.model_engine = model_engine 51 | 52 | def send_msg(self, msg): 53 | return self.model.chat_completions(msg, self.model_engine) 54 | 55 | def summarize(self, chunks): 56 | text = '\n'.join(chunks)[:self.text_length_limit] 57 | msgs = [{ 58 | "role": "system", "content": self.system_message 59 | }, { 60 | "role": "user", "content": self.message_format.format(text) 61 | }] 62 | return self.send_msg(msgs) 63 | -------------------------------------------------------------------------------- /src/service/youtube.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import re 4 | from src.utils import get_role_and_content 5 | 6 | from youtube_transcript_api import YouTubeTranscriptApi, NoTranscriptFound, TranscriptsDisabled 7 | 8 | 9 | YOUTUBE_SYSTEM_MESSAGE = "你現在非常擅於做資料的整理、總結、歸納、統整,並能專注於細節、且能提出觀點" 10 | PART_MESSAGE_FORMAT = """ PART {} START 11 | 下面是一個 Youtube 影片的部分字幕: \"\"\"{}\"\"\" \n\n請總結出這部影片的重點與一些細節,字數約 100 字左右 12 | PART {} END 13 | """ 14 | WHOLE_MESSAGE_FORMAT = "下面是每一個部分的小結論:\"\"\"{}\"\"\" \n\n 請給我全部小結論的總結,字數約 100 字左右" 15 | SINGLE_MESSAGE_FORMAT = "下面是一個 Youtube 影片的字幕: \"\"\"{}\"\"\" \n\n請總結出這部影片的重點與一些細節,字數約 100 字左右" 16 | 17 | 18 | class Youtube: 19 | def __init__(self, step): 20 | self.step = step 21 | self.chunk_size = 150 22 | 23 | def get_transcript_chunks(self, video_id): 24 | try: 25 | transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['zh-TW', 'zh', 'ja', 'zh-Hant', 'zh-Hans', 'en', 'ko']) 26 | text = [t.get('text') for i, t in enumerate(transcript) if i % self.step == 0] 27 | chunks = ['\n'.join(text[i*self.chunk_size: (i+1)*self.chunk_size]) for i in range(math.ceil(len(text) / self.chunk_size))] 28 | except NoTranscriptFound: 29 | return False, [], '目前只支援:中文、英文、日文、韓文' 30 | except TranscriptsDisabled: 31 | return False, [], '本影片無開啟字幕功能' 32 | except Exception as e: 33 | return False, [], str(e) 34 | return True, chunks, None 35 | 36 | def retrieve_video_id(self, url): 37 | regex = r'(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})' 38 | match = re.search(regex, url) 39 | if match: 40 | return match.group(1) 41 | else: 42 | return None 43 | 44 | 45 | class YoutubeTranscriptReader: 46 | def __init__(self, model=None, model_engine=None): 47 | self.summary_system_prompt = os.getenv('YOUTUBE_SYSTEM_MESSAGE') or YOUTUBE_SYSTEM_MESSAGE 48 | self.part_message_format = os.getenv('PART_MESSAGE_FORMAT') or PART_MESSAGE_FORMAT 49 | self.whole_message_format = os.getenv('WHOLE_MESSAGE_FORMAT') or WHOLE_MESSAGE_FORMAT 50 | self.single_message_format = os.getenv('SINGLE_MESSAGE_FORMAT') or SINGLE_MESSAGE_FORMAT 51 | self.model = model 52 | self.model_engine = model_engine 53 | 54 | def send_msg(self, msg): 55 | return self.model.chat_completions(msg, self.model_engine) 56 | 57 | def summarize(self, chunks): 58 | summary_msg = [] 59 | if len(chunks) > 1: 60 | for i, chunk in enumerate(chunks): 61 | msgs = [{ 62 | "role": "system", "content": self.summary_system_prompt 63 | }, { 64 | "role": "user", "content": self.part_message_format.format(i, chunk, i) 65 | }] 66 | _, response, _ = self.send_msg(msgs) 67 | _, content = get_role_and_content(response) 68 | summary_msg.append(content) 69 | text = '\n'.join(summary_msg) 70 | msgs = [{ 71 | 'role': 'system', 'content': self.summary_system_prompt 72 | }, { 73 | 'role': 'user', 'content': self.whole_message_format.format(text) 74 | }] 75 | else: 76 | text = chunks[0] 77 | msgs = [{ 78 | 'role': 'system', 'content': self.summary_system_prompt 79 | }, { 80 | 'role': 'user', 'content': self.single_message_format.format(text) 81 | }] 82 | return self.send_msg(msgs) 83 | -------------------------------------------------------------------------------- /src/storage.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | 4 | 5 | class FileStorage: 6 | def __init__(self, file_name): 7 | self.fine_name = file_name 8 | self.history = {} 9 | 10 | def save(self, data): 11 | self.history.update(data) 12 | with open(self.fine_name, 'w', newline='') as f: 13 | json.dump(self.history, f) 14 | 15 | def load(self): 16 | with open(self.fine_name, newline='') as jsonfile: 17 | data = json.load(jsonfile) 18 | self.history = data 19 | return self.history 20 | 21 | 22 | class MongoStorage: 23 | def __init__(self, db): 24 | self.db = db 25 | 26 | def save(self, data): 27 | user_id, api_key = list(data.items())[0] 28 | self.db['api_key'].update_one({ 29 | 'user_id': user_id 30 | }, { 31 | '$set': { 32 | 'user_id': user_id, 33 | 'api_key': api_key, 34 | 'created_at': datetime.datetime.utcnow() 35 | } 36 | }, upsert=True) 37 | 38 | def load(self): 39 | data = list(self.db['api_key'].find()) 40 | res = {} 41 | for i in range(len(data)): 42 | res[data[i]['user_id']] = data[i]['api_key'] 43 | return res 44 | 45 | 46 | class Storage: 47 | def __init__(self, storage): 48 | self.storage = storage 49 | 50 | def save(self, data): 51 | self.storage.save(data) 52 | 53 | def load(self): 54 | return self.storage.load() 55 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | import opencc 2 | 3 | s2t_converter = opencc.OpenCC('s2t') 4 | t2s_converter = opencc.OpenCC('t2s') 5 | 6 | 7 | def get_role_and_content(response: str): 8 | role = response['choices'][0]['message']['role'] 9 | content = response['choices'][0]['message']['content'].strip() 10 | content = s2t_converter.convert(content) 11 | return role, content 12 | --------------------------------------------------------------------------------