├── .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://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 | 
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 | [
](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://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 | 
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 | [
](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 |
--------------------------------------------------------------------------------