"""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.tagging import find_related_tag, generate_tags from app.core.weather import fetch_tomorrow_weather, judge_laundry from app.core.weather_cache import set_latest from app.core.word_memory import record_message 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 = "こんにちは" record_message(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) # 振られた話題のタグを生成し、それに近しい話題からTipsのネタを作る seed_tags = await generate_tags(content) related_tag = find_related_tag(seed_tags) await send_useless_tip(topic_override=related_tag) 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) if reply: record_message(reply) 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")