├── .env.example
├── .gitattributes
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── README_EN.md
├── call_api.py
├── channel.json
├── docs
├── en
│ ├── commands_en.md
│ ├── log_en.md
│ ├── principles_en.md
│ ├── q2_en.md
│ ├── q3_en.md
│ ├── q4_en.md
│ ├── q6_en.md
│ └── q7_en.md
├── images
│ ├── 1.png
│ ├── 10.png
│ ├── 11.png
│ ├── 12.jpg
│ ├── 13.jpg
│ ├── 14.jpg
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ ├── 7.png
│ ├── 8.png
│ └── 9.png
└── zh
│ ├── commands.md
│ ├── log.md
│ ├── principles.md
│ ├── q2.md
│ ├── q3.md
│ ├── q4.md
│ ├── q6.md
│ └── q7.md
├── main.py
├── manual.txt
├── prompt.txt
├── requirements.txt
├── spider.py
└── tools.py
/.env.example:
--------------------------------------------------------------------------------
1 | DISCORD_TOKEN = discord bot token
2 | GEMINI_API_KEY = gemini api key
3 | PREFIX = !
4 | MODE = whitelist # Enter "blacklist" or "whitelist"
5 | MEMORY_MAX = 100
6 |
7 | # Tools
8 | CALL_TOOLS = false # Enter "true" or "fasle"
9 | APP_ID = wolframalpha app id
10 | GOOGLE_API_KEY = Google Custom Search API Key
11 | SEARCH_ENGINE_ID = Google Custom Search API Search Engine ID
12 | YOUTUBE_API_KEY = YouTube Data API v3 API Key
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.12-slim
2 | WORKDIR /app
3 | ENV PYTHONUNBUFFERED=1
4 | COPY . /app
5 | RUN pip install -r requirements.txt
6 | CMD ["python", "main.py"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 yimang
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gemini Discord Bot
2 | [](https://www.python.org/)
3 | [](https://github.com/Rapptz/discord.py)
4 | [](https://github.com/imyimang/discord-gemini-chat-bot/stargazers)
5 | [](https://github.com/imyimang/discord-gemini-chat-bot/forks)
6 | [](https://github.com/imyimang/discord-gemini-chat-bot/blob/main/LICENSE)
7 |
8 | ## [English](README_EN.md) | 繁體中文
9 |
10 | 這是一個利用 Google Gemini 模型的 API 來製作的 Discord AI 聊天機器人
11 |
12 | ## 功能
13 | - 短期記憶
14 | - 圖片辨識
15 | - 爬取網址 Title 簡單判斷內容
16 | - 透過 Wolframalpha 搜尋內容
17 | - 透過 Google 搜尋內容
18 | - 爬取 NOWnews 各版新聞
19 | - 搜尋 Youtube 影片
20 |
21 | ## Demo
22 |
23 | 點擊查看
24 |
25 |
26 |
27 | ## 安裝
28 | 將機器人設定填入 `.env.example` 中,然後將它重新命名為 `.env`
29 |
30 | > [!WARNING]
31 | > 外部工具調用預設為關閉,如果要開啟請將 `CALL_TOOLS = false` 更改為 `CALL_TOOLS = true`
32 | > 並填入對應的 API Key
33 |
34 | 安裝函式庫:
35 | ```powershell
36 | pip install -U -r requirements.txt
37 | ```
38 | 將 prompt 放入 `prompt.txt` (可略過) [教學](docs/zh/q7.md)
39 |
40 | 將 history 放入 `call_api.py` (可略過) [教學](docs/zh/q3.md)
41 |
42 | 執行 `main.py`
43 |
44 |
45 | ## 介紹
46 | - [運作原理](docs/zh/principles.md)
47 | - [指令](docs/zh/commands.md)
48 | - [更新日誌](docs/zh/log.md)
49 |
50 | ## 常見問題
51 | - [如何取得 Gemini API key?](docs/zh/q2.md)
52 | - [如何撰寫提示詞?](docs/zh/q7.md)
53 | - [如何產生訓練用的history?](docs/zh/q3.md)
54 | - [Error:The caller does not have permisson](docs/zh/q4.md)
55 | - [Gemini 不同模型的選擇](docs/zh/q6.md)
56 |
57 |
58 | # 參考資料
59 | - [Echoshard/Gemini_Discordbot](https://github.com/Echoshard/Gemini_Discordbot)
60 | - [peter995peter/discord-gemini-ai](https://github.com/peter995peter/discord-gemini-ai)
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 | # Gemini Discord Bot
2 | [](https://www.python.org/)
3 | [](https://github.com/Rapptz/discord.py)
4 | [](https://github.com/imyimang/discord-gemini-chat-bot/stargazers)
5 | [](https://github.com/imyimang/discord-gemini-chat-bot/forks)
6 | [](https://github.com/imyimang/discord-gemini-chat-bot/blob/main/LICENSE)
7 |
8 | ## English | [繁體中文](README.md)
9 |
10 | This is a Discord AI chatbot created using the Google Gemini model's API.
11 |
12 | ## Features
13 | - Short-term memory
14 | - Image recognition
15 | - Simple web link title extraction and content detection
16 | - Content search via WolframAlpha
17 | - Content search via Google
18 | - News crawling from various NOWnews sections
19 | - YouTube video search
20 |
21 | ## Demo
22 |
23 | Click to view
24 |
25 |
26 |
27 | ## Installation
28 | Fill in the bot configuration in `.env.example`, then rename it to `.env`.
29 |
30 | > [!WARNING]
31 | > External tool calling is disabled by default. To enable it, change `CALL_TOOLS = false` to `CALL_TOOLS = true`,
32 | > and provide the corresponding API keys.
33 |
34 | Install dependencies:
35 | ```powershell
36 | pip install -U -r requirements.txt
37 | ```
38 |
39 | Put your prompt into `prompt.txt` (optional) [Tutorial](docs/zh/q7.md)
40 |
41 | Place your message history logic into `call_api.py` (optional) [Tutorial](docs/zh/q3.md)
42 |
43 | Run `main.py`
44 |
45 | ## Introduction
46 | - [How it works](docs/en/principles_en.md)
47 | - [Commands](docs/en/commands_en.md)
48 | - [Changelog](docs/en/log_en.md)
49 |
50 | ## FAQ
51 | - [How to get a Gemini API key?](docs/en/q2_en.md)
52 | - [How to write prompts?](docs/en/q7_en.md)
53 | - [How to generate training history?](docs/en/q3_en.md)
54 | - [Error: The caller does not have permission](docs/en/q4_en.md)
55 | - [Different Gemini model options](docs/en/q6_en.md)
56 |
57 | # References
58 | - [Echoshard/Gemini_Discordbot](https://github.com/Echoshard/Gemini_Discordbot)
59 | - [peter995peter/discord-gemini-ai](https://github.com/peter995peter/discord-gemini-ai)
--------------------------------------------------------------------------------
/call_api.py:
--------------------------------------------------------------------------------
1 | import google.generativeai as genai
2 | import os
3 | from dotenv import load_dotenv
4 |
5 | load_dotenv()
6 |
7 | API_KEY = os.getenv("GEMINI_API_KEY")
8 | genai.configure(api_key=API_KEY)
9 |
10 | # 模型設定, 詳細設定請去Gemini官方文件研究
11 | generation_config = {
12 | 'temperature': 1,
13 | 'top_p': 1,
14 | 'top_k': 1,
15 | 'max_output_tokens': 2048,
16 | }
17 |
18 | # 安全設定, 建議可以照著下面的
19 | safety_settings = [
20 | {
21 | 'category': 'HARM_CATEGORY_HARASSMENT',
22 | 'threshold': 'block_none'
23 | },
24 | {
25 | 'category': 'HARM_CATEGORY_HATE_SPEECH',
26 | 'threshold': 'block_none'
27 | },
28 | {
29 | 'category': 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
30 | 'threshold': 'block_none'
31 | },
32 | {
33 | 'category': 'HARM_CATEGORY_DANGEROUS_CONTENT',
34 | 'threshold': 'block_none'
35 | },
36 | ]
37 |
38 | model = genai.GenerativeModel(model_name='gemini-1.5-flash', generation_config=generation_config, safety_settings=safety_settings) # 設定模型, 這邊不用動他
39 |
40 | image_model = genai.GenerativeModel(model_name='gemini-1.5-flash', generation_config=generation_config, safety_settings=safety_settings) # 定義另外一個 model 用來生成圖片回應 (兩者不能相容)
41 |
42 |
43 | if os.getenv("CALL_TOOLS", "false").lower() == "false":
44 | with open("prompt.txt", "r", encoding="utf-8") as f:
45 | prompt = f.read()
46 | else:
47 | with open("prompt.txt", "r", encoding="utf-8") as f:
48 | with open("manual.txt", "r", encoding="utf-8") as f2:
49 | prompt = f.read()+ f2.read()
50 |
51 | #==============================================================
52 |
53 | async def text_api(msg: str) -> str | None:
54 | '''
55 | 呼叫 api 並回傳他的回應
56 | '''
57 | convo = model.start_chat(history=[
58 | #==========================================
59 | # 這裡放你的 history / put your history here
60 | #==========================================
61 | ])
62 |
63 | if not msg: return '這段訊息是空的'
64 | await convo.send_message_async(msg) # 傳送 msg 內容給 Gemini api
65 | return convo.last.text # 將 api 的回應返還給主程式
66 |
67 | async def image_api(image_data) -> str:
68 | '''
69 | 回傳 api 對包含圖片的訊息的回應
70 | '''
71 | image_parts = [{'mime_type': 'image/jpeg', 'data': image_data}]
72 |
73 | # (下) 如果 text 不為空, 就用 text 依據文字內容來生成回應, 如果為空, 就依據 '這張圖片代表什麼?給我更多細節' 來生成回應
74 | prompt_parts = [image_parts[0], "這張圖片代表什麼? 給我更多細節"]
75 | response = image_model.generate_content(prompt_parts)
76 |
77 | if response._error: return '無法分析這張圖'
78 |
79 | return response.text
--------------------------------------------------------------------------------
/channel.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | }
--------------------------------------------------------------------------------
/docs/en/commands_en.md:
--------------------------------------------------------------------------------
1 | ## Commands (defaulting to blacklist mode)
2 |
3 | *Note: The following commands have no permission restrictions and can be used by anyone.*
4 |
5 | - Whitelist mode(Default)
6 | - openchannel ➡️ add channel to whitelist
7 | - closechannel ➡️ remove channel from whitelist
8 | - Blacklist mode
9 | - blockchannel ➡️ add channel to blacklist
10 | - unblockchannel ➡️ remove channel from blacklist
11 | - reset ➡️ Clearing short-term memory in the channel
12 |
13 | ## Points to note
14 | - All commands above are prefixed commands.
15 | - Command can't be used in DM channel(Except for the reset command)
16 | - This bot will reply **every message**,**don't need mention (@)**,so if you sent message too quick,it might get stuck.
17 | - In whitelist mode, only the reset command can be used in DM channels; opening or closing channels is not allowed.
18 | - **Short-term memory for each channel is separate**, the reset command only clears the short-term memory of the specified channel.
19 | - The maximum sentence limit of short-term memory includes responses from the bot.
20 | - If the mode specified in config.json is not "blacklist" or "whitelist", it will cause the bot to be unable to use commands.
21 |
--------------------------------------------------------------------------------
/docs/en/log_en.md:
--------------------------------------------------------------------------------
1 | - 2024/5/23
2 | - Merge main_whitelist.py into main.py
3 | - Optimize the formatting
4 | - Change the commands to prefixed commands
5 |
6 | - 2024/6/8
7 | - Streamline the code to allow the reset command to be used in DM channels
8 |
9 | - 2024/6/14
10 | - Modified the file and function names to make them more intuitive to use
11 |
12 | - 2024/6/19
13 | - The issues regarding the inability to detect command messages and inability to use have been fixed
14 | - Integrating image recognition results into short-term memory
15 |
16 | - 2024/6/20
17 | - Modified the structure of the prompt, splitting it into two parts: 'history' and 'prompt'
18 |
19 | - 2024/7/28
20 | - Because Google AI no longer supports Gemini Pro Vision, the image recognition model has been changed to Gemini 1.5 Flash
21 |
22 | - 2024/8/5
23 | - Fixed the issue where the whitelist mode could not be used in DM channels
24 |
25 | - 2024/10/13
26 | - Fixed an issue where messages containing multiple URLs could not be processed by the web scraper
27 |
28 | - 2025/02/16
29 | - Fix the issue where the web scraper is not working and add a Dockerfile
30 |
31 | - 2025/03/15
32 | - Since the Google Gemini API no longer supports the gemini-1.0-pro model, the image recognition model has been switched to gemini-1.5-pro
33 |
34 | - 2025/03/26
35 | - Change image response structure
36 |
37 | - 2025/04/20
38 | - Added external tool invocation, supporting reading of `.txt`, `.log`, and `.md` files
--------------------------------------------------------------------------------
/docs/en/principles_en.md:
--------------------------------------------------------------------------------
1 | The bot's memory is stored in the form of Python dictionaries in memory, while channel.json is responsible for storing channel blacklists/whitelists.
2 |
3 | ## Memory principle
4 | ```py
5 | {'channel_id1': 'value1', 'channel_id2': 'value2', 'channel_id3': 'value3'}
6 | ```
7 | When a user sends a message, the message is stored in a dictionary with the channel ID corresponding to the value, and then the value is sent to the API, thus achieving the ability of short-term memory.
8 |
9 | **It's not long-term memory; the data in memory will disappear once the system restarts.**
10 |
11 | > [!WARNING]
12 | > Once the number of servers reaches a certain threshold, blacklist mode may cause on_message detection to become very slow, and it may even miss detections.
13 |
--------------------------------------------------------------------------------
/docs/en/q2_en.md:
--------------------------------------------------------------------------------
1 | ## How to get Gemini api key
2 |
3 | 1. Visit the Gemini website. [**click me**]()
4 |
5 | 2. Log in to your Google account (account must be at least 18 years old) [**Click here for details**]()
6 |
7 | 3. Check the box to agree to the terms of service and then click "Continue".
8 | 
9 |
10 | 4. Click "Get API key"
11 | 
12 |
13 | 5. Click "Create API key" (if you don't have project yet then click "create api key in new project")
14 | 
15 |
16 | 6. You've now obtained the API key. Click "Copy" to copy it.
17 | 
--------------------------------------------------------------------------------
/docs/en/q3_en.md:
--------------------------------------------------------------------------------
1 | ## How to generate history for training
2 | 1. Visit the Gemini website. [**click me**]()
3 |
4 | 2. Click "Create new",then choose "Chat prompt"
5 | 
6 |
7 | 3.Write something and click "Save"
8 | 
9 | You can directly click "Save" without naming if you prefer.
10 |
11 | 4.Export your history.
12 | Click "Get code" to export
13 | 
14 |
15 | Choose "Python"
16 |
17 | This segment is your history.
18 | 
19 |
20 | 5.Go back to `call_api.py`
21 |
22 | 
23 |
24 | You just need to replace the highlighted segment with your own history.
25 |
26 | 
27 |
28 | * "User" is the question you want to ask
29 | * "Model" is where you specify how you want it to respond
30 |
31 | You can also directly use conversation transcripts and place the entity you want to simulate under "Model" and yourself under "User".
32 |
33 | Simply copy the format of the history and change the content inside.
--------------------------------------------------------------------------------
/docs/en/q4_en.md:
--------------------------------------------------------------------------------
1 | ## The caller does not have permisson
2 |
3 | When you click "Create API key," you might encounter the following error.
4 |
5 | **The caller does not have permisson**
6 |
7 | This might be because you deleted your previous API key. It's recommended to create a new API key with another account that hasn't created an API key before.
--------------------------------------------------------------------------------
/docs/en/q6_en.md:
--------------------------------------------------------------------------------
1 | ## Choosing between different models in Gemini
2 | Gemini released free versions 1.0 and 1.5, both of which are free to use. The relevant rate limits are as shown in the image below.
3 | 
4 |
5 | If you're using the free version, it's recommended to use 1.0. Although 1.5 has optimized models, its rate limits are quite strict.
6 |
7 | To change the model, simply go to `call_api.py`
8 | ```py
9 | ...
10 |
11 | model = genai.GenerativeModel(model_name='gemini-1.0-pro', generation_config=generation_config, safety_settings=safety_settings)
12 |
13 | image_model = genai.GenerativeModel(model_name='gemini-pro-vision', generation_config=generation_config, safety_settings=safety_settings)
14 |
15 | ...
16 | ```
17 | This part
18 |
19 | Change
20 | ```py
21 | model_name='gemini-1.0-pro'
22 | ```
23 | to
24 | ```py
25 | model_name='gemini-1.5-pro'
26 | ```
27 | You can make changes according to the model you want.
28 | > [!WARNING]
29 | > Starting from July 12, 2024, Google AI will no longer support Gemini 1.0 Pro Vision api. Please use Gemini 1.5 Flash or other models.
30 |
31 | > [!WARNING]
32 | > Starting from February 18, 2025, Google AI will no longer support Gemini 1.0 Pro API. Please use Gemini 1.5 Flash/Pro or other models.
--------------------------------------------------------------------------------
/docs/en/q7_en.md:
--------------------------------------------------------------------------------
1 | ## How to write prompt
2 |
3 | You can refer to the following method for writing prompts
4 |
5 | * Establish clear rules, avoid overly conversational language, and provide specific instructions.
6 |
7 | * Try to have him play a specific role (programmer, writer, etc.).
8 |
9 | * The character setting can be generated using ChatGPT.
10 |
11 | * You can specify that he respond in a particular language.
12 |
13 | Here is a sample prompt:
14 | ```
15 | "(From now on, all conversations will be in English) Please follow these rules for future conversations:
16 | Please generate a response to the 'last sentence' of the conversation partner based on your tone. Do not generate a conversation record.
17 | Try to condense the generated content into 2-3 sentences.
18 | When you notice repetitive conversation content, you can end that topic and start a new one.
19 | All future responses must follow this format: '[Me]:(response text)'.
20 | Please adhere to all the above rules."
21 | ```
22 |
23 | After writing the prompt, put it into the 'prompt' section of the `call_api.py`
--------------------------------------------------------------------------------
/docs/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/1.png
--------------------------------------------------------------------------------
/docs/images/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/10.png
--------------------------------------------------------------------------------
/docs/images/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/11.png
--------------------------------------------------------------------------------
/docs/images/12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/12.jpg
--------------------------------------------------------------------------------
/docs/images/13.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/13.jpg
--------------------------------------------------------------------------------
/docs/images/14.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/14.jpg
--------------------------------------------------------------------------------
/docs/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/2.png
--------------------------------------------------------------------------------
/docs/images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/3.png
--------------------------------------------------------------------------------
/docs/images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/4.png
--------------------------------------------------------------------------------
/docs/images/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/5.png
--------------------------------------------------------------------------------
/docs/images/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/6.png
--------------------------------------------------------------------------------
/docs/images/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/7.png
--------------------------------------------------------------------------------
/docs/images/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/8.png
--------------------------------------------------------------------------------
/docs/images/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/docs/images/9.png
--------------------------------------------------------------------------------
/docs/zh/commands.md:
--------------------------------------------------------------------------------
1 | ## 指令 (預設為黑名單模式)
2 |
3 | *注意: 以下指令都沒有設定任何權限限制,任何人都能使用。*
4 |
5 | - 白名單模式(預設)
6 | - openchannel ➡️ 新增頻道至白名單
7 | - closechannel ➡️ 將頻道從白名單移除
8 | - 黑名單模式
9 | - blockchannel ➡️ 新增頻道至黑名單
10 | - unblockchannel ➡️ 將頻道從黑名單移除
11 | - reset ➡️ 清空頻道短期記憶
12 |
13 | ## 注意事項
14 | - 以上指令皆為前綴指令。
15 | - 指令在私訊 (DM channel) 無法使用。
16 | - 這個版本是 **每則訊息都會回覆**,**不需要 mention (@)**,所以一次太多訊息他會卡住。
17 | - 在白名單模式下DM channel只能使用reset指令,無法開啟或關閉頻道
18 | - **每個頻道的短期記憶是分開的**,reset 指令只會清空指定頻道的短期記憶。
19 | - 短期記憶的上限包含機器人的回覆。
20 | - 如果 config.json 中 mode 填寫的不是 blacklist 或 whitelist,會導致機器人無法使用指令。
--------------------------------------------------------------------------------
/docs/zh/log.md:
--------------------------------------------------------------------------------
1 | - 2024/5/23
2 | - 將黑白名單合併到main.py
3 | - 優化排版
4 | - 將指令改為前綴指令
5 |
6 | - 2024/6/8
7 | - 精簡程式碼,讓reset指令能在DM channel中使用
8 |
9 | - 2024/6/14
10 | - 修改了一下檔案和函式名稱,讓使用比較直觀
11 |
12 | - 2024/6/19
13 | - 修復了無法判定指令訊息和無法使用的問題
14 | - 將圖片辨識結果加入短期記憶中
15 |
16 | - 2024/6/20
17 | - 修改了prompt的架構,把它分成history和prompt兩部分
18 |
19 | - 2024/7/28
20 | - 因為Google AI不再支援Gemini 1.0 Pro Vision,所以將圖像辨識模型改成Gemini 1.5 Flash
21 |
22 | - 2024/8/5
23 | - 修復了白名單模式在DM channel無法使用的問題
24 |
25 | - 2024/10/13
26 | - 修復了一則訊息包含多個網址爬蟲無法處理的問題
27 |
28 | - 2025/02/16
29 | - 修復爬蟲無法使用,新增Dockerfile
30 |
31 | - 2025/03/15
32 | - 由於Google Gemini API不再支援 gemini-1.0-pro 模型,圖片辨識的模型改為用 gemini-1.5-pro
33 |
34 | - 2025/03/26
35 | - 更改圖片回應架構
36 |
37 | - 2025/04/20
38 | - 新增外部工具調用,支援讀取 .txt / .log / .md 檔案
--------------------------------------------------------------------------------
/docs/zh/principles.md:
--------------------------------------------------------------------------------
1 | 機器人記憶是用 Python 字典的格式來儲存在記憶體中,channel.json 則負責記憶頻道黑/白名單。
2 |
3 | ## 記憶邏輯
4 | 格式如下
5 | ```py
6 | {'頻道id1': 'value1', '頻道id2': 'value2', '頻道id3': 'value3'}
7 | ```
8 | 當用戶發送訊息時,會將訊息儲存到字典中頻道 id 對應的 value,然後再將值發送給api,藉此達成短期記憶的能力。
9 |
10 | **不過並非長期記憶,只要重開機記憶體裡面的資料就會消失。**
11 |
12 | > [!WARNING]
13 | > 當伺服器數量達到一定數量後,黑名單模式可能會導致on_message偵測十分緩慢,甚至遺漏偵測
14 |
--------------------------------------------------------------------------------
/docs/zh/q2.md:
--------------------------------------------------------------------------------
1 | ## 如何取得 Gemini api key?
2 |
3 | 1. 前往 Gemini的網站 [**點我**]()
4 |
5 | 2. 登入你的 Google 帳號 (要帳戶年滿 18 歲) [**詳情點我**]()
6 |
7 | 3. 打勾同意用戶條款然後按繼續
8 | 
9 |
10 | 4. 點 "Get API key"
11 | 
12 |
13 | 5. 點 "Create API key" (如果還沒有建立專案就點 "create api key in new project")
14 | 
15 |
16 | 6. 這樣就得到 API key 了,點 "Copy" 就能複製
17 | 
--------------------------------------------------------------------------------
/docs/zh/q3.md:
--------------------------------------------------------------------------------
1 | ## 如何產生訓練用的history?
2 | 1. 前往 Gemini 的網站 [**點我**]()
3 |
4 | 2. 點 "Create new",然後選 "Chat prompt"
5 | 
6 |
7 | 3.先隨便打點東西然後按 "Save"
8 | 
9 | 名字不取也沒關係可以直接按 "Save"
10 |
11 | 4.匯出你的history
12 | 點擊右上角的 "Get code" 就能匯出
13 | 
14 |
15 | 選 "Python" 然後往下滑一點
16 |
17 | 這段就是你的history了
18 | 
19 |
20 | 5.回到 `call_api.py`
21 |
22 | 
23 |
24 | 把框起來的那段換成自己的 history 就可以囉
25 |
26 | 
27 |
28 | * "User" 的地方就是你要問的問題
29 | * "Model" 的地方就是你想要他怎麼回答
30 |
31 | 也可以直接用對話紀錄然後把你想要模擬的對象放在 "Model" 的地方,自己放在 "User" 的地方
32 |
33 | 只要複製 history 的格式並且更改裡面內容即可
--------------------------------------------------------------------------------
/docs/zh/q4.md:
--------------------------------------------------------------------------------
1 | ## The caller does not have permisson
2 |
3 | 當你點擊 "Create API key" 的時候,可能會出現下面的錯誤
4 |
5 | **The caller does not have permisson**
6 |
7 | 這可能是由於你刪除了你的前一個 api key。建議用另一個沒創過 api key 的帳號,重新創建一個 api key。
--------------------------------------------------------------------------------
/docs/zh/q6.md:
--------------------------------------------------------------------------------
1 | ## Gemini 不同模型的選擇
2 | 近期 Gemini 有釋出了免費的 1.0 和 1.5 版本,都是可以免費使用,相關的限速限制如下圖
3 | 
4 |
5 | 如果是使用免費版本的話建議使用 1.0,1.5 雖然模型經過優化但限速很嚴重。
6 |
7 | 更改模型只要到 `call_api.py` 的
8 | ```py
9 | ...
10 |
11 | model = genai.GenerativeModel(model_name='gemini-1.0-pro', generation_config=generation_config, safety_settings=safety_settings) # 設定模型, 這邊不用動他
12 |
13 | image_model = genai.GenerativeModel(model_name='gemini-pro-vision', generation_config=generation_config, safety_settings=safety_settings)
14 |
15 | ...
16 | ```
17 | 這一段
18 |
19 | 將
20 | ```py
21 | model_name='gemini-1.0-pro'
22 | ```
23 | 更改為
24 | ```py
25 | model_name='gemini-1.5-pro'
26 | ```
27 | 即可
28 | 可以根據自身想要的模型做更改。
29 |
30 | > [!WARNING]
31 | > 從2024/07/12開始,Google AI不再支援 Gemini 1.0 Pro Vision API,請使用 Gemini 1.5 Flash 或其他模型
32 |
33 | > [!WARNING]
34 | > 從2025/02/18開始,Google AI不再支援 Gemini 1.0 Pro API,請使用 Gemini 1.5 Flash/Pro 或其他模型
--------------------------------------------------------------------------------
/docs/zh/q7.md:
--------------------------------------------------------------------------------
1 | ## 如何撰寫Prompt提示詞?
2 |
3 | prompt的寫法可以參考以下方法
4 |
5 | * 制定明確規則,不要太過口語話,要給明確的指令
6 |
7 | * 嘗試讓他扮演某種角色(程式設計師,作家等等)
8 |
9 | * 人設部分可以利用chatgpt來生成
10 |
11 | * 可以指定他用某種特定語言回應
12 |
13 | 以下是一段prompt範例
14 | ```
15 | (以後對話全部用繁體中文)以後對話請依照以下規則:
16 | 1.請依照你的口氣對聊天對象的'最後一句話'生成回應內容,不要生成對話紀錄,
17 | 2.生成內容盡量濃縮在2~3句,
18 | 3.當你發現聊天內容重複時可以結束該話題並開啟新的話題,
19 | 4.以後所有回答都要依照以下格式'[我]:(回應的文字)',
20 | 5.請遵守以上所有規則
21 | ```
22 | 寫完prompt後放進 `prompt.txt` 即可
23 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands, tasks
3 | import re, json, aiohttp, os
4 | import aiofiles
5 | from itertools import cycle
6 | from dotenv import load_dotenv
7 | from call_api import prompt, text_api, image_api
8 | from spider import islink, gettitle
9 | from tools import wolframalpha,get_news,youtube_search
10 |
11 | load_dotenv()
12 |
13 | TOKEN = os.getenv("DISCORD_TOKEN")
14 | PREFIX = os.getenv("PREFIX")
15 | MODE = os.getenv("MODE", "whitelist")
16 | MEMORY_MAX = int(os.getenv("MEMORY_MAX", 100))
17 |
18 | # Functions
19 | # ==================================================
20 | def update_message_history(channel_id: int, text: str) -> str:
21 | if channel_id not in log:
22 | log[channel_id] = []
23 | log[channel_id].append(text)
24 | if len(log[channel_id]) > MEMORY_MAX:
25 | log[channel_id].pop(0)
26 | return "\n".join(log[channel_id])
27 |
28 | def format_discord_message(input_string: str) -> str:
29 | '''
30 | 刪除並回傳 Discord 聊天訊息中位於 < 和 > 之間的文字 (讓他能夠放入短期記憶並被 AI 讀懂)
31 | '''
32 | bracket_pattern = re.compile(r'<[^>]+>')
33 | cleaned_content = bracket_pattern.sub('', input_string)
34 | return cleaned_content
35 |
36 | def get_message_history(channel_id: int) -> str | None:
37 | '''
38 | 回傳指定頻道的短期記憶
39 | '''
40 | if channel_id in log: # 如果 channel_id 有在 log 字典裏面
41 | return '\n\n'.join(log[channel_id])
42 |
43 | def load_channel_data(channel: discord.abc.GuildChannel) -> tuple[str, list]:
44 | '''
45 | 讀取並回傳資料
46 | '''
47 | current_dir = os.path.dirname(__file__)
48 | channel_path = os.path.join(current_dir, 'channel.json')
49 |
50 | with open(channel_path, 'r', encoding='utf-8') as file: # 打開 json 檔案
51 | data: dict = json.load(file)
52 |
53 | if 'id' not in data:
54 | data['id'] = []
55 | save_data(data, "channel")
56 |
57 | channel_list: list = data['id'] # 定義 channel_list 為 json 裡面鍵值為 'id' 的資料
58 |
59 | return str(channel.id), channel_list, data
60 |
61 | def save_data(data: dict, data_file: str):
62 | '''
63 | 儲存檔案
64 | '''
65 | current_dir = os.path.dirname(__file__)
66 | file_path = os.path.join(current_dir, f'{data_file}.json')
67 |
68 | with open(file_path, 'w', encoding='utf-8') as file:
69 | json.dump(data, file, ensure_ascii=False, indent=4)
70 |
71 | def extract_json_block(text):
72 | """
73 | 從文字中尋找並解析第一個合法的 JSON 區塊(支援巢狀),
74 | 返回 (解析後的 JSON 物件, 起始位置, 結束位置) 或 None。
75 | """
76 | start = text.find("{")
77 | while start != -1:
78 | stack = []
79 | for i in range(start, len(text)):
80 | if text[i] == "{":
81 | stack.append("{")
82 | elif text[i] == "}":
83 | if stack:
84 | stack.pop()
85 | if not stack: # 找到完整的 JSON 區塊
86 | json_str = text[start:i+1]
87 | try:
88 | parsed_json = json.loads(json_str)
89 | return parsed_json, start, i + 1
90 | except json.JSONDecodeError:
91 | break # JSON 格式錯誤,換下一組
92 | start = text.find("{", start + 1)
93 | return None, -1, -1
94 |
95 | async def process_tools_in_response(channel_id: int, response: str) -> str:
96 | print("原始回應:", response)
97 | all_tool_outputs = [] # 儲存所有工具的執行結果
98 | processed_jsons = set() # 記錄已處理的 JSON 字符串,避免重複
99 | max_iterations = 20 # 限制最大迭代次數,防止無限循環
100 |
101 | iteration = 0
102 | while iteration < max_iterations:
103 | data, start, end = extract_json_block(response)
104 | if not data:
105 | print("未找到新的 JSON 區塊,結束提取")
106 | break
107 |
108 | # 將 JSON 轉為字符串,用於檢查是否已處理
109 | json_str = json.dumps(data, sort_keys=True)
110 | if json_str in processed_jsons:
111 | print(f"檢測到重複的 JSON 區塊:{json_str},跳過")
112 | # 移除重複的 JSON 區塊,防止下次循環再次提取
113 | response = response[:start] + response[end:]
114 | iteration += 1
115 | continue
116 |
117 | tool_response = None
118 | if data.get("type") == "wolframalpha" and data.get("question_original") and data.get("question_english"):
119 | print(f"正在使用 wolframalpha,內容原文:{data["question_original"]},內容英文:{data["question_english"]}")
120 | tool_response = wolframalpha(data["question_english"], data["question_original"])
121 |
122 | elif data.get("type") == "get_news" and data.get("category"):
123 | print(f"正在使用 get_news,類別:{data['category']}")
124 | tool_response = get_news(data["category"])
125 |
126 | elif data.get("type") == "youtube_search" and data.get("query") and data.get("max_results") and data.get("language") and data.get("duration"):
127 | print(f"正在使用 youtube_search,內容:{data['query']}")
128 | tool_response = await youtube_search(data["query"], int(data["max_results"]), data["language"], data["duration"])
129 |
130 | # 移除當前 JSON 區塊(無論是否有有效工具回應)
131 | response = response[:start] + response[end:]
132 | print("移除 JSON 後的回應:", response)
133 |
134 | if tool_response:
135 | print("工具結果:", tool_response)
136 | all_tool_outputs.append(tool_response)
137 | processed_jsons.add(json_str) # 記錄已處理的 JSON
138 | else:
139 | print(f"無效工具或缺少參數,跳過 JSON:{json_str}")
140 | processed_jsons.add(json_str) # 即使無效也要記錄,避免重複處理
141 |
142 | iteration += 1
143 |
144 | # 如果有工具結果,則一次性送回模型處理
145 | if all_tool_outputs:
146 | print("所有工具結果:", all_tool_outputs)
147 | # 將工具結果合併到系統提示中,並明確要求模型不要生成新的工具 JSON
148 | history = update_message_history(
149 | channel_id,
150 | "[system]: (模型已調用外部工具,結果如下,請根據結果回應使用者的問題,並避免生成新的工具 JSON 區塊)\n" +
151 | "\n".join(all_tool_outputs)
152 | )
153 | response = await text_api(prompt + history)
154 | update_message_history(channel_id, "[model]: " + response)
155 | else:
156 | print("無工具結果,直接返回原始回應")
157 |
158 | return response
159 | # ==================================================
160 |
161 | log: dict[int, list[str]] = {} # 創建一個名稱叫 log 的字典, 用來存放短期記憶
162 |
163 | # 檢查 MODE 是否有效
164 | while True:
165 | if MODE in ['whitelist', 'blacklist']: break
166 | MODE = input('不明的模式,模式應為 "whitelist" 或 "blacklist"\n輸入執行模式: ')
167 |
168 | bot = commands.Bot(command_prefix=commands.when_mentioned_or(PREFIX), intents=discord.Intents.all())
169 |
170 | status = cycle(['Gemini chat bot', '我是 AI 機器人', '正在聊天']) # 機器人顯示的個人狀態,可自行更改
171 |
172 | @tasks.loop(seconds=10) # 每隔 10 秒更換一次機器人個人狀態
173 | async def change_status():
174 | await bot.change_presence(activity=discord.Game(next(status)))
175 |
176 | @bot.event
177 | async def on_ready():
178 | print(f'{bot.user} 已上線,正在執行 {"白名單" if MODE == "whitelist" else "黑名單"} 模式!')
179 | change_status.start() # 讓機器人顯示狀態
180 |
181 | # Commands
182 | # ==================================================
183 | if MODE == 'whitelist':
184 | @bot.command()
185 | @commands.guild_only()
186 | async def openchannel(ctx: commands.Context, channel: discord.abc.GuildChannel = None):
187 | '''
188 | 新增白名單內頻道
189 | '''
190 | channel = channel or ctx.channel
191 |
192 | channel_id, channel_list, data = load_channel_data(channel)
193 |
194 | if channel_id not in channel_list: # 如果頻道 id 未被記錄在 json 檔案
195 | channel_list.append(channel_id) # 新增 channel_id 這筆資料
196 |
197 | data['id'] = channel_list
198 | save_data(data, "channel")
199 |
200 | await ctx.reply('頻道已成功開啟 AI 聊天。', mention_author=False)
201 |
202 | @bot.command()
203 | @commands.guild_only()
204 | async def closechannel(ctx: commands.Context, channel: discord.abc.GuildChannel = None):
205 | '''
206 | 移除白名單內頻道
207 | '''
208 | channel = channel or ctx.channel
209 |
210 | channel_id, channel_list, data = load_channel_data(channel)
211 |
212 | if channel_id in channel_list: # 如果頻道 id 已被記錄在 json 檔案
213 | channel_list.remove(channel_id) # 移除 channel_id 這筆資料
214 |
215 | data['id'] = channel_list
216 | save_data(data, "channel")
217 |
218 | await ctx.reply('頻道已成功關閉 AI 聊天。', mention_author=False)
219 |
220 | elif MODE == "blacklist":
221 | @bot.command()
222 | @commands.guild_only()
223 | async def blockchannel(ctx: commands.Context, channel: discord.abc.GuildChannel = None):
224 | '''
225 | 新增黑名單內頻道
226 | '''
227 | channel = channel or ctx.channel
228 |
229 | channel_id, channel_list, data = load_channel_data(channel)
230 |
231 | if channel_id not in channel_list: # 如果頻道 id 未被記錄在 json 檔案
232 | channel_list.append(channel_id) # 新增 channel_id 這筆資料
233 |
234 | data['id'] = channel_list
235 | save_data(data, "channel")
236 |
237 | await ctx.reply('頻道已成功屏蔽。', mention_author=False)
238 |
239 | @bot.command()
240 | @commands.guild_only()
241 | async def unblockchannel(ctx: commands.Context, channel: discord.abc.GuildChannel = None):
242 | '''
243 | 移除黑名單內頻道
244 | '''
245 | channel = channel or ctx.channel
246 |
247 | channel_id, channel_list, data = load_channel_data(channel)
248 |
249 | if channel_id in channel_list: # 如果頻道 id 已被記錄在 json 檔案
250 | channel_list.remove(channel_id) # 移除 channel_id 這筆資料
251 |
252 | data['id'] = channel_list
253 | save_data(data, "channel")
254 |
255 | await ctx.reply('頻道已成功解除屏蔽。', mention_author=False)
256 |
257 | @bot.command()
258 | async def reset(ctx: commands.Context, channel: discord.abc.Messageable = None):
259 | '''
260 | 清空頻道短期記憶
261 | '''
262 | channel = channel or ctx.channel
263 |
264 | if channel.id in log:
265 | del log[channel.id] # 清空短期記憶
266 | if not isinstance(ctx.channel, discord.DMChannel):
267 | await ctx.reply(f'{channel.mention} 的短期記憶已清空。', mention_author=False)
268 | else:
269 | await ctx.reply(f'本私訊的短期記憶已清空。', mention_author=False)
270 | else:
271 | await ctx.reply('並無儲存的短期記憶。', mention_author=False)
272 | # ==================================================
273 |
274 | import os
275 |
276 | @bot.listen('on_message')
277 | async def handle_message(msg: discord.Message):
278 | if msg.author == bot.user:
279 | return
280 |
281 | command_name = msg.content.removeprefix(PREFIX)
282 | if (command_name in [cmd.name for cmd in bot.commands]):
283 | return
284 |
285 | if not isinstance(msg.channel, discord.DMChannel):
286 | can_send = msg.channel.permissions_for(msg.guild.me).send_messages
287 | if not can_send:
288 | print(f'沒有權限在此頻道 ({msg.channel.name}) 發言。')
289 | return
290 |
291 | result = load_channel_data(msg.channel)
292 | channel_id, channel_list = result[0], result[1]
293 | if ((MODE == 'whitelist' and channel_id not in channel_list) or
294 | (MODE == 'blacklist' and channel_id in channel_list)) and not isinstance(msg.channel, discord.DMChannel):
295 | return
296 |
297 | async with msg.channel.typing():
298 | attachment_info = None
299 |
300 | # 處理圖片與文字檔附件
301 | if msg.attachments:
302 | for attachment in msg.attachments:
303 | filename = attachment.filename.lower()
304 |
305 | # 圖片處理
306 | if any(filename.endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp']):
307 | async with aiohttp.ClientSession() as session:
308 | async with session.get(attachment.url) as resp:
309 | if resp.status != 200:
310 | await msg.reply('圖片載入失敗。', mention_author=False)
311 | return
312 | print(f'正在分析 {msg.author.name} 的圖片...')
313 | image_data = await resp.read()
314 | response_text = await image_api(image_data)
315 | attachment_info = f"(附上一張圖片,內容是「{response_text}」)"
316 |
317 | # 純文字檔處理
318 | elif any(filename.endswith(ext) for ext in ['.txt', '.md', '.log']):
319 | file_path = f"temp_{msg.id}_{filename}"
320 | async with aiohttp.ClientSession() as session:
321 | async with session.get(attachment.url) as resp:
322 | if resp.status != 200:
323 | await msg.reply('文字檔案載入失敗。', mention_author=False)
324 | return
325 | f = await aiofiles.open(file_path, mode='wb')
326 | await f.write(await resp.read())
327 | await f.close()
328 | try:
329 | f = await aiofiles.open(file_path, mode='r', encoding='utf-8', errors='ignore')
330 | text_data = await f.read()
331 | await f.close()
332 | print(f'使用者上傳的文字檔內容:\n{text_data[:500]}')
333 | attachment_info = f"(附上一個文字檔,內容是「{text_data[:300]}...」)"
334 | finally:
335 | if os.path.exists(file_path):
336 | os.remove(file_path)
337 |
338 | # 處理網址
339 | word = msg.content
340 | links = islink(msg.content)
341 | if links:
342 | for link in links:
343 | title = gettitle(link)
344 | word = word.replace(link, f'(一個網址, 網址標題是: "{title}")\n' if title else '(一個網址, 網址無法辨識)\n')
345 |
346 | if attachment_info:
347 | word += f"\n{attachment_info}"
348 |
349 | dc_msg = format_discord_message(word)
350 | update_message_history(msg.channel.id, f"[{msg.author.name}]:{dc_msg}")
351 | reply_text = await text_api(prompt + get_message_history(msg.channel.id))
352 |
353 | # 根據環境變數決定是否要處理工具
354 | if os.getenv("CALL_TOOLS", "false").lower() == "true":
355 | reply_text = await process_tools_in_response(msg.channel.id, reply_text)
356 |
357 | await msg.reply(reply_text.replace("[model]:", ""), mention_author=False, allowed_mentions=discord.AllowedMentions.none())
358 | update_message_history(msg.channel.id, f"[model]:" + reply_text)
359 |
360 | # 除錯印出
361 | print(f'Server name: {msg.guild.name if not isinstance(msg.channel, discord.DMChannel) else "私訊"}')
362 | print(f'Message ID: {msg.id}')
363 | print(f'Message Content: {msg.content}')
364 | print(f'Author ID: {msg.author.id}')
365 | print(f'Author Name: {msg.author.name}')
366 | if not isinstance(msg.channel, discord.DMChannel):
367 | print(f'Channel name: {msg.channel.name}')
368 | print(f'Channel id: {msg.channel.id}')
369 | print("附件摘要:", attachment_info)
370 | print("模型回覆:\n", reply_text)
371 |
372 | bot.run(TOKEN)
--------------------------------------------------------------------------------
/manual.txt:
--------------------------------------------------------------------------------
1 | ====
2 | 你是一個Discord聊天機器人,會用自然語言和用戶聊天。
3 |
4 | 你的聊天語氣應該是輕鬆自然幽默。
5 |
6 | 除非用戶暗示否則你不應該調用外部工具
7 |
8 | 你應該在聊天途中根據情況適時的調用外部工具。
9 |
10 | 你只能使用下面列出的工具。
11 |
12 | 當你需要使用工具時,請只輸出 JSON 格式的指令,禁止輸出任何文字、表情符號、Markdown標籤、標點符號或自然語言解釋。
13 |
14 | ⚠️ 禁止違規:輸出格式開頭必須是 `{`,結尾是 `}`。中間不能有任何自然語言或解釋文字,不能講「等等我一下」或是「我查查看」,也不能加 emoji 或標點,**只能回傳 JSON**
15 |
16 | 根據使用者的需求,你可以組合多個JSON來一次呼叫多個工具。
17 | 範例:
18 | """
19 | {
20 | "type": "wolframalpha",
21 | "question_original": "搜尋內容(原文)"
22 | "question_english": "搜尋內容(翻譯成英文)"
23 | }
24 | 然後
25 | {
26 | "type": "get_news",
27 | "category": "sport"
28 | }
29 | """
30 | ----
31 | 當你覺得使用者的問題需要上網搜尋,請使用以下格式,"question_english"請務必使用英文,"question_original"使用原文:
32 | """
33 | {
34 | "type": "wolframalpha",
35 | "question_original": "搜尋內容(原文)"
36 | "question_english": "搜尋內容(翻譯成英文)"
37 | }
38 | """
39 | -----
40 |
41 | 當你覺得使用者的問題需要搜尋新聞時,請使用以下格式,選擇一個新聞類別:
42 | """
43 | {
44 | "type": "get_news",
45 | "category": "breaking"(即時) | "news-summary"(要聞) | "entertainment"(娛樂) | "sport"(運動) | "news-global"(全球) | "finance"(財經) | "society-vientiane"(社會) | "house2"(房產) | "life"(生活) | "4wayvoice"(外籍移工) | "local"(地方) | "gama"(校園)
46 | }
47 | """
48 | -----
49 |
50 | 當你需要從 Youtube 搜尋影片時,請使用以下格式,並選擇搜尋數量和語言(ISO 639-1 語言代碼)和影片長度:
51 | """
52 | {
53 | "type": "youtube_search",
54 | "query": "搜尋內容",
55 | "max_results": "搜尋影片數量(必須小於10)",
56 | "language": "en" | "zh-TW",
57 | "duration": "short"(小於4分鐘) | "medium"(4-20 分鐘) | "long"(大於20分鐘)
58 | }
59 | """
60 | Youtube影片可以使用markdown標籤回應給使用者 ex:[標題](連結)
61 | ===
62 | 以下是你和使用者的對話,請根據歷史紀錄來回應最後一句
63 | ===
--------------------------------------------------------------------------------
/prompt.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imyimang/discord-gemini-chat-bot/8c20f1d21fabf0458a9b0fee6541ddc72363ad00/prompt.txt
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | discord
2 | google-generativeai
3 | aiohttp
4 | beautifulsoup4
5 | requests
6 | python-dotenv
7 | isodate
8 | google-api-python-client
9 | aiofiles
--------------------------------------------------------------------------------
/spider.py:
--------------------------------------------------------------------------------
1 | import requests, re
2 | from bs4 import BeautifulSoup
3 |
4 | def gettitle(website_url: str) -> str | None:
5 | '''
6 | 取得網頁的 title 標籤並回傳
7 | '''
8 | try:
9 | response = requests.get(website_url) # 發送 GET 請求
10 |
11 | soup = BeautifulSoup(response.text, 'html.parser') # 使用 BeautifulSoup 解析 HTML
12 |
13 | print(f'The title of the website is: {soup.title.string}') # 印出 title
14 | return soup.title.string
15 |
16 | except Exception:
17 | return None
18 |
19 | def islink(content: str) -> str: # 判定訊息是否是連結
20 | return re.findall(r'https?://\S+', content) # 如果是連結就返回連結 + 連結以外的文字
--------------------------------------------------------------------------------
/tools.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import urllib.parse
3 | import xml.etree.ElementTree as ET
4 | from bs4 import BeautifulSoup
5 | import re
6 | import os
7 | from dotenv import load_dotenv
8 | from googleapiclient.discovery import build
9 | from googleapiclient.errors import HttpError
10 | from datetime import datetime
11 | import isodate
12 |
13 | load_dotenv()
14 |
15 | APP_ID = os.getenv('APP_ID')
16 | GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
17 | SEARCH_ENGINE_ID = os.getenv('SEARCH_ENGINE_ID')
18 | YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY')
19 | url = "https://www.googleapis.com/customsearch/v1"
20 |
21 | def google(s):
22 | params = {
23 | "key": GOOGLE_API_KEY,
24 | "cx": SEARCH_ENGINE_ID,
25 | "q": s,
26 | }
27 |
28 | response = requests.get(url, params=params)
29 | results = response.json()
30 |
31 | output = []
32 |
33 | for item in results.get("items", []):
34 | title = "標題: " + item["title"]
35 | about = "簡介: " + item.get("snippet", "")
36 | link = "連結: " + item.get("link")
37 | output.append(f"{title}\n{about}\n{link}\n-----\n")
38 |
39 | return "搜尋結果: " + "\n".join(output) if output else "找不到任何結果"
40 |
41 |
42 | def wolframalpha(query: str, query_or : str):
43 | encoded_query = urllib.parse.quote(query)
44 | url = f"https://api.wolframalpha.com/v2/query?appid={APP_ID}&input={encoded_query}"
45 |
46 | response = requests.get(url)
47 |
48 | if response.status_code == 200:
49 | root = ET.fromstring(response.content)
50 | results = []
51 |
52 | for pod in root.findall(".//pod"):
53 | title = pod.attrib.get("title")
54 | texts = []
55 | for subpod in pod.findall("subpod"):
56 | plaintext = subpod.find("plaintext")
57 | if plaintext is not None and plaintext.text:
58 | texts.append(plaintext.text.strip())
59 |
60 | if texts:
61 | # 把 title 和內容整理成一段段文字
62 | result_block = f"【{title}】\n" + "\n".join(texts)
63 | results.append(result_block)
64 |
65 | if results:
66 | return "\n\n".join(results)
67 | else:
68 | return "wolframalpha搜尋沒有結果,google搜尋結果如下" + google(query_or)
69 | else:
70 | return f"WolframAlpha 回傳錯誤:{response.status_code}"
71 |
72 | def get_news(category):
73 | result = ""
74 | category_url = f"https://www.nownews.com/cat/{category}/"
75 |
76 | headers = {
77 | "User-Agent": "Mozilla/5.0"
78 | }
79 |
80 | res = requests.get(category_url, headers=headers)
81 | res.encoding = "utf-8"
82 | soup = BeautifulSoup(res.text, "html.parser")
83 |
84 | left_col = soup.find("div", class_="leftCol")
85 | links = left_col.find_all("a", class_="trace-click")
86 |
87 | visited = set()
88 | count = 0
89 |
90 | for link in links:
91 | if count >= 5:
92 | break
93 |
94 | href = link.get("href")
95 | if href and href.startswith("https://www.nownews.com/news/") and href not in visited:
96 | visited.add(href)
97 |
98 | try:
99 | article_res = requests.get(href, headers=headers)
100 | article_res.encoding = "utf-8"
101 | article_soup = BeautifulSoup(article_res.text, "html.parser")
102 |
103 | title_tag = article_soup.find("h1", class_="article-title")
104 | title = title_tag.text.strip() if title_tag else "標題未找到"
105 |
106 | content_div = article_soup.find("div", id="articleContent")
107 | if content_div:
108 | for ad in content_div.find_all(class_=["ad-blk", "ad-blk1"]):
109 | ad.decompose()
110 |
111 | for br in content_div.find_all("br"):
112 | br.replace_with("\n")
113 |
114 | text = content_div.get_text(strip=True, separator="\n")
115 |
116 | # 移除 ▲ 開頭的整行
117 | text = re.sub(r'^▲.*(?:\n|$)', '', text, flags=re.MULTILINE)
118 | text = "\n".join([line for line in text.split("\n") if line.strip()])
119 | else:
120 | text = "找不到文章內容"
121 |
122 | # 累加到結果字串
123 | result += f"📰 標題:{title}\n📄 內文:\n{text}\n\n{'='*10}\n\n"
124 | count += 1
125 |
126 | except Exception as e:
127 | print(f"⚠️ 無法處理 {href}:{e}")
128 |
129 | return result
130 |
131 |
132 | async def youtube_search(query, max_results=5, language="en", duration="medium"):
133 | try:
134 | # 初始化 YouTube API 客戶端
135 | youtube = build("youtube", "v3", developerKey=YOUTUBE_API_KEY)
136 |
137 | # 設置搜尋參數
138 | search_params = {
139 | "part": "id,snippet",
140 | "q": query,
141 | "type": "video",
142 | "maxResults": min(max_results, 50), # API 限制最多 50 條
143 | "relevanceLanguage": language,
144 | "order": "relevance" # 可改為 "viewCount" 或 "date"
145 | }
146 |
147 | # 篩選影片時長
148 | if duration in ["short", "medium", "long"]:
149 | search_params["videoDuration"] = duration
150 |
151 | # 執行搜尋請求
152 | request = youtube.search().list(**search_params)
153 | response = request.execute()
154 |
155 | # 提取影片 ID 清單
156 | video_ids = [item["id"]["videoId"] for item in response.get("items", [])]
157 |
158 | if not video_ids:
159 | return f"找不到與 '{query}' 相關的影片,請嘗試其他關鍵詞。"
160 |
161 | # 查詢影片詳細資訊(包括時長)
162 | details_request = youtube.videos().list(
163 | part="snippet,contentDetails",
164 | id=",".join(video_ids)
165 | )
166 | details_response = details_request.execute()
167 |
168 | # 格式化結果
169 | results = []
170 | for item in details_response.get("items", []):
171 | snippet = item["snippet"]
172 | content_details = item["contentDetails"]
173 | # 解析影片時長(ISO 8601 格式)
174 | duration = isodate.parse_duration(content_details["duration"])
175 | duration_str = f"{duration.seconds // 3600}h {duration.seconds % 3600 // 60}m" if duration.seconds >= 3600 else f"{duration.seconds // 60}m {duration.seconds % 60}s"
176 | # 解析發布日期
177 | published_at = datetime.strptime(snippet["publishedAt"], "%Y-%m-%dT%H:%M:%SZ").strftime("%Y-%m-%d")
178 | # 構建結果條目
179 | result = (
180 | f"[{snippet['title']}](https://www.youtube.com/watch?v={item['id']}) - "
181 | f"由 {snippet['channelTitle']} 發布,時長 {duration_str},{published_at}"
182 | )
183 | results.append(result)
184 |
185 | # 返回格式化的清單
186 | return f"\n以下是 Youtube上 '{query}' 的搜尋結果:\n" + "\n".join(f"{i+1}. {result}" for i, result in enumerate(results))
187 |
188 | except HttpError as e:
189 | return f"YouTube API 請求失敗:{str(e)}"
190 | except Exception as e:
191 | return f"搜尋影片時發生錯誤:{str(e)}"
--------------------------------------------------------------------------------