├── .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 | [![Python](https://img.shields.io/badge/python-%3E%3D%203.12-blue)](https://www.python.org/) 3 | [![discord.py](https://img.shields.io/badge/discord.py-%3E%3D%202.4.0-blue)](https://github.com/Rapptz/discord.py) 4 | [![Stars](https://img.shields.io/github/stars/imyimang/discord-gemini-chat-bot)](https://github.com/imyimang/discord-gemini-chat-bot/stargazers) 5 | [![Forks](https://img.shields.io/github/forks/imyimang/discord-gemini-chat-bot)](https://github.com/imyimang/discord-gemini-chat-bot/forks) 6 | [![License](https://img.shields.io/github/license/imyimang/discord-gemini-chat-bot)](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 | Image 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 | [![Python](https://img.shields.io/badge/python-%3E%3D%203.12-blue)](https://www.python.org/) 3 | [![discord.py](https://img.shields.io/badge/discord.py-%3E%3D%202.4.0-blue)](https://github.com/Rapptz/discord.py) 4 | [![Stars](https://img.shields.io/github/stars/imyimang/discord-gemini-chat-bot)](https://github.com/imyimang/discord-gemini-chat-bot/stargazers) 5 | [![Forks](https://img.shields.io/github/forks/imyimang/discord-gemini-chat-bot)](https://github.com/imyimang/discord-gemini-chat-bot/forks) 6 | [![License](https://img.shields.io/github/license/imyimang/discord-gemini-chat-bot)](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 | Image 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 | ![圖1](../images/1.png) 9 | 10 | 4. Click "Get API key" 11 | ![圖2](../images/2.png) 12 | 13 | 5. Click "Create API key" (if you don't have project yet then click "create api key in new project") 14 | ![圖3](../images/3.png) 15 | 16 | 6. You've now obtained the API key. Click "Copy" to copy it. 17 | ![圖4](../images/4.png) -------------------------------------------------------------------------------- /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 | ![圖5](../images/5.png) 6 | 7 | 3.Write something and click "Save" 8 | ![圖6](../images/6.png) 9 | You can directly click "Save" without naming if you prefer. 10 | 11 | 4.Export your history. 12 | Click "Get code" to export 13 | ![圖7](../images/7.png) 14 | 15 | Choose "Python" 16 | 17 | This segment is your history. 18 | ![圖9](../images/8.png) 19 | 20 | 5.Go back to `call_api.py` 21 | 22 | ![圖11](../images/9.png) 23 | 24 | You just need to replace the highlighted segment with your own history. 25 | 26 | ![圖12](../images/10.png) 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 | ![圖13](../images/11.png) 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 | ![圖1](../images/1.png) 9 | 10 | 4. 點 "Get API key" 11 | ![圖2](../images/2.png) 12 | 13 | 5. 點 "Create API key" (如果還沒有建立專案就點 "create api key in new project") 14 | ![圖3](../images/3.png) 15 | 16 | 6. 這樣就得到 API key 了,點 "Copy" 就能複製 17 | ![圖4](../images/4.png) -------------------------------------------------------------------------------- /docs/zh/q3.md: -------------------------------------------------------------------------------- 1 | ## 如何產生訓練用的history? 2 | 1. 前往 Gemini 的網站 [**點我**]() 3 | 4 | 2. 點 "Create new",然後選 "Chat prompt" 5 | ![圖5](../images/5.png) 6 | 7 | 3.先隨便打點東西然後按 "Save" 8 | ![圖6](../images/6.png) 9 | 名字不取也沒關係可以直接按 "Save" 10 | 11 | 4.匯出你的history 12 | 點擊右上角的 "Get code" 就能匯出 13 | ![圖7](../images/7.png) 14 | 15 | 選 "Python" 然後往下滑一點 16 | 17 | 這段就是你的history了 18 | ![圖9](../images/8.png) 19 | 20 | 5.回到 `call_api.py` 21 | 22 | ![圖11](../images/9.png) 23 | 24 | 把框起來的那段換成自己的 history 就可以囉 25 | 26 | ![圖12](../images/10.png) 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 | ![圖13](../images/11.png) 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)}" --------------------------------------------------------------------------------