毎日Discordにオススメ記事を配信してくれるAI Botを一瞬で作る!
以下の様な最新記事を毎日配信してくれる兄貴がいたらありがたいですよね!
今回は、FastAPIとGemini APIを使用して、LambdaとEventBridgeでサクっとBotを作成する方法をまとめました!
説明はいいから、さっさとコードだけ見せろやって方はコチラ👇
使用する技術は以下になります。Gemini APIが無料なので今回はGeminiを選びました。
- Python3.12
- FastAPI
- Lambda
- EventBridge
- Docker
- Gemini API
事前準備
DiscordのWebhook URLを発行します。
Botを召喚したいチャンネルの設定から、Botを作成して、Webhook URLを保存してください。
アイコン画像は、ChatGPTに作成してもらいました。
構成
今回は、serverlessフレームワークを使用し、コマンド一発でデプロイできるような構成にします。
AWS上のWeb UIを触らずにローカルでもコマンドを2つ実行するだけで簡単にデプロイが完了します。
今回EventBridgeを使用して定期実行しますが、実行する時間帯の設定などもファイルに定義しておくことで、自動でデプロイされるようにします。
.
├── app
│ ├── .env
│ ├── __init__.py
│ ├── main.py
│ └── src
│ ├── logger.py
│ ├── service.py
│ └── settings.py
├── Dockerfile
├── requirements.txt
└── serverless.yml
serverless.ymlの設定
serverless.yml
ファイルを作成します。
ここの設定さえしっかりすれば後はコマンドでデプロイするだけです。
service: discord-rss-bot-test
frameworkVersion: "4"
plugins:
- serverless-python-requirements
provider:
name: aws
runtime: python3.12
memorySize: 512
architecture: x86_64
region: ap-northeast-1
timeout: 30
apiGateway:
binaryMediaTypes:
- "*/*"
ecr:
images:
discord-rss-bot-test:
path: ./
platform: linux/amd64
functions:
app:
events:
- eventBridge:
schedule: cron(0 3 * * ? *)
input:
bot_name: developer
image:
name: discord-rss-bot-test
command:
- app.main.handler
package:
individually: true
eventBridgeの設定を記述します。
schedule
にcronでチャンネルに投稿する時間を指定します。(cronについての詳しい解説は省きます。)
今回は、日本時間の午後12時に配信するために、UTCで3
を指定しました。
bot_name
については、LambdaのAPI発火時にコード上で受け取る変数になります。後ほど説明します。
Dockerfileを作成
FROM --platform=linux/amd64 public.ecr.aws/lambda/python:3.12
COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["app.main.handler"]
serverless.yml
ファイルのimage
にて指定するDocker imageをbuildするために作成します。
main.pyの作成
app
ディレクトリを作成して、その中にmain.pyを作成します。
from app.src.logger import logger
from app.src.service import call_llm
def handler(event, context):
logger.info(event)
bot_name = event.get("bot_name")
if bot_name == "developer":
theme = "エンジニア"
bot_name = "兄貴"
urls = [
"https://www.publickey1.jp/atom.xml",
"https://ai-data-base.com/feed",
"https://zenn.dev/topics/flutter/feed",
]
discord_url = "https://discord.com/api/webhooks/hogehoge"
call_main(urls, theme, bot_name, discord_url)
else:
logger.info("other")
return {"message": event}
if __name__ == "__main__":
# test
handler({"bot_name": "developer"}, None)
theme
、bot_name
は、Geminiへのプロンプトを作成する際に使用するために、定義しています。
また、urls
は配信してほしいRSSのURLのリストになります。
discord_url
には、事前準備で取得したDiscordのWebhook URLを入れます。
call_main
にて、RSS記事取得からDiscordへのポストの処理を行います。後ほど、この関数について解説します。
event.get("bot_name")
にて、serverless.yml
で指定したbot_name
の値を取得できます。serverless.yml
のeventBridge
の設定を増やす事で複数のBotを作成できるような仕様にしました。
ローカルでテストする場合は、以下のコマンドを実行してください。
python -m app.main
RSSを取得する関数を実装する
app
ディレクトリ配下にsrc
ディレクトリを作成します。
app
ディレクトリ配下にservice.py
を作成して、ここに細かい関数を実装していきます。
まずは、必要なライブラリをimportします。
import feedparser
from dataclasses import dataclass
from typing import List
import requests
import json
from datetime import datetime, timedelta, timezone
from dateutil import parser
import time
from app.src import logger
from app.src.settings import GOOGLE_AI_STUDIO_API_KEY
RSS記事を取得する関数を実装します。
@dataclass
class RssContent:
title: str
url: str
published: str
def get_rss(endpoint: str) -> List[RssContent]:
feed = feedparser.parse(endpoint)
rss_list: List[RssContent] = []
for entry in feed.entries:
if not entry.get("link"):
continue
rss_content = RssContent(
title=entry.title, url=entry.link, published=entry.published
)
rss_list.append(rss_content)
return rss_list
Gemini APIを呼び出す関数
Geminiを呼び出す関数を実装します。
def call_gemini(page_content: str, theme: str, bot_name: str):
prompt = f"""あなたはRSSを発信する{bot_name}です。{bot_name}のような口調でRSS(テーマ:{theme})の内容を500文字以内で要約してください。改行は含めないでください。
<info>
{page_content}
</info>
"""
uri = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={GOOGLE_AI_STUDIO_API_KEY}"
headers = {"Content-Type": "application/json"}
data = {
"contents": [{"parts": [{"text": prompt}]}],
}
response = requests.post(uri, headers=headers, data=json.dumps(data))
json_data = response.json()
response_text = (
json_data.get("candidates")[0].get("content").get("parts")[0].get("text")
)
return response_text
bot_name
、theme
はmain.py
で定義した値が入ります。
theme = "エンジニア"
bot_name = "兄貴"
環境変数から、GOOGLE_AI_STUDIO_API_KEY
を取得しています。
.envファイルに設定されている変数をsettings.py
に定義しています。
import os
from os.path import join, dirname
from dotenv import load_dotenv
load_dotenv(verbose=True)
dotenv_path = join(dirname(__file__), ".env")
load_dotenv(dotenv_path)
GOOGLE_AI_STUDIO_API_KEY = os.environ.get("GOOGLE_AI_STUDIO_API_KEY")
全体処理の実装
call_main
というmain.py
から呼び出す全体処理の関数を作成します。
今まで実装してきたget_rss()
、call_gemini()
関数はここで使用します。
def call_main(urls: List[str], theme: str, bot_name: str, discord_url: str):
# URLsからRSSのリストを取得
rss_list = []
for url in urls:
rss_list.extend(get_rss(url))
# publishedが24時間以内のものを取得
rss_list = [
rss
for rss in rss_list
if datetime.now(timezone.utc) - parser.parse(rss.published) < timedelta(days=1)
]
# rss_listを最新順に並び替える
rss_list = sorted(
rss_list,
key=lambda x: parser.parse(x.published),
reverse=False,
)
# rss_listを10件ずつ分割する
rss_lists = [rss_list[i : i + 10] for i in range(0, len(rss_list), 10)]
for rss_list in rss_lists:
# RSSの記事のタイトル一覧を作成(リンクを含む)
parsed_content = "".join(
[f"{i+1}. [{rss.title}]({rss.url})\n" for i, rss in enumerate(rss_list)]
)
# RSSの記事のタイトル一覧を作成
content = "".join([f"{i}. {rss.title}\n" for i, rss in enumerate(rss_list)])
for _ in range(3): # 最大3回リトライ
try:
res = call_gemini(content, theme, bot_name)
break
except Exception as e:
logger.error(f"エラーが発生しました: {e}")
time.sleep(1)
else:
res = "エラーが発生しました。"
requests.post(discord_url, json={"content": f"{res}\n{parsed_content}"})
今回は、毎日決まった時間にポストされるようにcron
で設定しました。なので、24時間以内の記事だけを取得するようにして、毎日のポストで重複した記事が含まれないようにしています。
また、Discordの無課金状態では、投稿の文字数制限があるため、10記事ずつ分割してポストするようにしました。
parsed_content
は、Discordに投稿するための、リンクを含むタイトルをマークダウン形式で記述したものになります。
content
は、Geminiのプロンプトで使用する記事のタイトル一覧になります。
call_gemini()
関数にリトライ処理を入れてるのは、Gemini APIのエラー(レート制限やLLMによる出力のブレ)対策です。
その他(logger)
CloudWatchにてログを見やすくするために、loggerを実装しました。
import logging
from logging.handlers import TimedRotatingFileHandler
import os
import sys
def setup_logging(log_dir="logs", log_file_name="info.log"):
aws_lambda_path = "/tmp/"
log_dir = os.path.join(aws_lambda_path, log_dir)
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_file_path = os.path.join(log_dir, log_file_name)
logger = logging.getLogger("Logger")
logger.setLevel(logging.INFO) # ログレベルの設定
handler = TimedRotatingFileHandler(
filename=log_file_path,
when="midnight",
interval=1,
encoding="utf-8",
backupCount=200,
)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
# StreamHandlerの追加
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
# ハンドラーをロガーに追加
logger.addHandler(handler)
logger.addHandler(console_handler)
return logger
logger = setup_logging()
デプロイ
実装は以上なので、実際にデプロイしてみましょう。
AWSの設定をします。
aws
コマンドのインストールなどの解説は省きます。
aws configure
serverlessコマンドのインストール
serverless
コマンドのインストールをします。
npm install -g serverless
venvによる仮想環境の構築
Pythonのバージョン管理としてvenv
を使用しました。
特にこだわりが無い場合は、この手順はスキップしても大丈夫かと思います。
python -m venv ./venv
source ./venv/bin/activate
ライブラリのインストール
必要なライブラリをインストールします。
boto3==1.35.54
botocore==1.35.54
certifi==2024.8.30
charset-normalizer==3.4.0
feedparser==6.0.11
idna==3.10
jmespath==1.0.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
requests==2.32.3
s3transfer==0.10.3
sgmllib3k==1.0.0
six==1.16.0
urllib3==2.2.3
pip install -r requirements.txt
Lambdaへのデプロイ
1. Docker Imageをbuildします。
docker build -t discord-rss-bot-test .
※Apple Siliconを使用する場合
docker build --platform linux/amd64 -t discord-rss-bot-test .
2. デプロイします。
sls deploy
終わりに
今回実装したコードは以下になります。
cloneして該当箇所を変更すれば、すぐにデプロイできるような状態になっています。
爆速で作りたい方は、以下のコードを変更するのが手っ取り早いと思います。
コメントにて質問があれば、なるべく早く回答いたします。
Discussion