- Cache the latest fetched tomorrow weather and expose it via /api/system/weather/latest - Add a header Clock widget showing date/time plus the cached weather summary - Redirect useless-talk mentions from the weather bot to the useless-tips webhook - Switch Bedrock auth from bearer token to standard IAM access key/secret Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
116 lines
4.7 KiB
Python
116 lines
4.7 KiB
Python
"""DiscordでメンションされたらplaychatのLLMが返信するBot。"""
|
||
import logging
|
||
|
||
import discord
|
||
|
||
from app.core.config import (
|
||
DISCORD_BOT_SYSTEM_PROMPT,
|
||
DISCORD_BOT_TOKEN,
|
||
WEATHER_AREA_CODE,
|
||
WEATHER_AREA_KEYWORD,
|
||
WEATHER_NOTIFY_MODEL,
|
||
)
|
||
from app.core.llm import complete
|
||
from app.core.scheduler import send_useless_tip
|
||
from app.core.weather import fetch_tomorrow_weather, judge_laundry
|
||
from app.core.weather_cache import set_latest
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
intents = discord.Intents.default()
|
||
intents.message_content = True
|
||
|
||
client = discord.Client(intents=intents)
|
||
|
||
WEATHER_KEYWORDS = ("天気", "天候", "洗濯", "雨", "気温", "降水")
|
||
USELESS_TALK_KEYWORDS = ("どうでもいい", "雑談", "暇", "つまんない", "退屈")
|
||
|
||
|
||
async def _build_weather_context() -> str:
|
||
"""明日の天気データを取得し、LLMに渡すコンテキスト文字列を作る。失敗時は取得不可の旨を返す。"""
|
||
try:
|
||
weather_data = await fetch_tomorrow_weather(WEATHER_AREA_CODE, WEATHER_AREA_KEYWORD)
|
||
judgement, reason = judge_laundry(weather_data["weather"], weather_data["pop"])
|
||
pop_text = f"{weather_data['pop']}%" if weather_data["pop"] is not None else "不明"
|
||
set_latest(weather_data["weather"], weather_data["pop"], judgement, reason)
|
||
return (
|
||
"【明日の天気予報データ(気象庁発表)】\n"
|
||
f"天気: {weather_data['weather']}\n"
|
||
f"降水確率: {pop_text}\n"
|
||
f"洗濯指数: {judgement}({reason})\n"
|
||
"このデータをもとにユーザーへ回答してください。"
|
||
)
|
||
except Exception:
|
||
logger.exception("Failed to fetch weather data for Discord reply")
|
||
return "(明日の天気データの取得に失敗しました。一般的な回答をしてください。)"
|
||
|
||
|
||
@client.event
|
||
async def on_ready():
|
||
logger.info("Discord bot logged in as %s", client.user)
|
||
|
||
|
||
@client.event
|
||
async def on_message(message: discord.Message):
|
||
if message.author.bot:
|
||
return
|
||
if client.user not in message.mentions:
|
||
return
|
||
|
||
content = message.content.replace(f"<@{client.user.id}>", "").strip()
|
||
if not content:
|
||
content = "こんにちは"
|
||
|
||
async with message.channel.typing():
|
||
try:
|
||
if any(keyword in content for keyword in USELESS_TALK_KEYWORDS):
|
||
redirect = await _generate_useless_talk_redirect(content)
|
||
await message.reply(redirect)
|
||
await send_useless_tip()
|
||
return
|
||
|
||
messages = [{"role": "system", "content": DISCORD_BOT_SYSTEM_PROMPT}]
|
||
|
||
if any(keyword in content for keyword in WEATHER_KEYWORDS):
|
||
weather_context = await _build_weather_context()
|
||
messages.append({"role": "system", "content": weather_context})
|
||
|
||
messages.append({"role": "user", "content": content})
|
||
|
||
reply = await complete(messages, model=WEATHER_NOTIFY_MODEL)
|
||
await message.reply(reply or "(応答が空でした)")
|
||
except Exception:
|
||
logger.exception("Failed to generate reply for Discord mention")
|
||
await message.reply("⚠️ 応答の生成に失敗しました")
|
||
|
||
|
||
async def _generate_useless_talk_redirect(content: str) -> str:
|
||
"""どうでもいい話を振られたとき、『どうでもいいTips』チャンネルへ誘導する一言を生成する。"""
|
||
fallback = "🤷 雑談なら『どうでもいいTips』チャンネルの出番です!今ちょうど一つ送りますね〜"
|
||
|
||
prompt = (
|
||
"あなたは「お天気アプリ」というDiscord Botです。"
|
||
"ユーザーがどうでもいい雑談を振ってきました。"
|
||
"あなたは天気専門なので、雑談は『どうでもいいTips』という別のBot/チャンネルにお任せしたいです。"
|
||
"そこへ話題を振るような、軽いノリの一言を1〜2文で返してください。"
|
||
"絵文字を1つ程度使い、出力は本文のみにしてください。\n\n"
|
||
f"ユーザーの発言: {content}"
|
||
)
|
||
|
||
try:
|
||
result = await complete([{"role": "user", "content": prompt}], model=WEATHER_NOTIFY_MODEL)
|
||
return result.strip() or fallback
|
||
except Exception:
|
||
logger.exception("Failed to generate useless talk redirect message")
|
||
return fallback
|
||
|
||
|
||
async def start_discord_bot() -> None:
|
||
if not DISCORD_BOT_TOKEN:
|
||
logger.warning("DISCORD_BOT_TOKEN is not set; Discord bot will not start")
|
||
return
|
||
try:
|
||
await client.start(DISCORD_BOT_TOKEN)
|
||
except Exception:
|
||
logger.exception("Discord bot failed to start")
|