"""毎日決まった時刻に天気予報と洗濯指数をDiscordへ送信するジョブ。""" import logging from datetime import datetime from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.interval import IntervalTrigger from app.core.config import ( DISCORD_WEBHOOK_URL, DISCORD_WEBHOOK_URL_TIPS, USELESS_TIPS_ENABLED, USELESS_TIPS_INTERVAL_HOURS, USELESS_TIPS_MODEL, WEATHER_AREA_CODE, WEATHER_AREA_KEYWORD, WEATHER_NOTIFY_HOUR, WEATHER_NOTIFY_MINUTE, WEATHER_NOTIFY_MODEL, ) from app.core.llm import complete from app.core.discord.notify import send_discord_message from app.core.weather.weather import fetch_tomorrow_weather, judge_laundry from app.core.weather.weather_cache import set_latest from app.core.memory_kb.word_memory import pick_random_word logger = logging.getLogger(__name__) scheduler = AsyncIOScheduler() async def send_weather_laundry_notification() -> None: if not DISCORD_WEBHOOK_URL: logger.warning("DISCORD_WEBHOOK_URL is not set; skipping weather notification") return 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) content = await _generate_message(weather_data["weather"], pop_text, judgement, reason) await send_discord_message(DISCORD_WEBHOOK_URL, content) except Exception: logger.exception("Failed to send weather/laundry notification") async def _generate_message(weather: str, pop_text: str, judgement: str, reason: str) -> str: """LLMに天気データを渡し、Discord向けのメッセージ文を生成させる。失敗時は固定テンプレートにフォールバックする。""" fallback = ( "🌤️ **明日の天気予報**\n" f"天気: {weather}\n" f"降水確率: {pop_text}\n\n" f"👕 **洗濯物**: {judgement}\n" f"({reason})" ) prompt = ( "以下の天気データをもとに、Discordに送る通知メッセージを作成してください。\n" "- 絵文字を使って読みやすく\n" "- 「明日の天気」と「洗濯物を外に干すべきか」の2点を必ず含める\n" "- 簡潔に3〜5行程度\n" "- Markdown装飾(**太字**など)はDiscordの記法に合わせて可\n" "- 出力はメッセージ本文のみ(前置きや説明文は不要)\n\n" f"天気: {weather}\n" f"降水確率: {pop_text}\n" f"洗濯判定: {judgement}(理由: {reason})" ) try: messages = [{"role": "user", "content": prompt}] result = await complete(messages, model=WEATHER_NOTIFY_MODEL) return result.strip() or fallback except Exception: logger.exception("LLM message generation failed; falling back to template") return fallback async def send_useless_tip(topic_override: str | None = None) -> None: """どうでもいいtipsをDiscordへ送信する。 topic_overrideを渡すと、ワードプールのランダム単語より優先してそのトピックでネタを作る。 雑談Botがタグベースで『近しい話題』を渡したいときに使う。 """ if not DISCORD_WEBHOOK_URL_TIPS: logger.warning("DISCORD_WEBHOOK_URL_TIPS is not set; skipping useless tip") return if datetime.now().hour == 3: logger.info("Skipping useless tip during 3am quiet hour") return topic_word = topic_override or pick_random_word() prompt = ( "あなたは『どうでもいいTips』という名前のDiscord Botです。" "あなた自身は、これから話す雑学が誰かの役に立つ、有益な豆知識だと心の底から信じ切っています。" "本人にその自覚は一切なく、本気の善意と謎の使命感で「これは知っておいた方がいい」と思って共有しています。\n\n" "実際の内容は、誰の役にも立たない、死ぬほどどうでもいい雑学・豆知識にしてください。\n" "- 全体で60文字以内(絵文字含む)に必ず収める。1文の短いつぶやきでよい\n" "- 「これは役に立ちますよ」「知っておくと得しますよ」というような、善意・自信たっぷりの前置きや一言を添える\n" "- ただし中身は本当にどうでもいい内容にする(このギャップが面白さのポイント)\n" "- 「どや顔」「これ豆な」的な、ちょっと得意げなノリにする\n" "- 絵文字を1つくらい添えてもよい\n" "- 出力は本文のみ(メタな説明やこの指示の繰り返しは不要)\n" "- 過去に何度も言われていそうな定番ネタは避け、できるだけ意外性のある内容にする" ) if topic_word: prompt += ( f"\n- 雑談Botでの会話に出てきた「{topic_word}」という話題に、こじつけでもいいので" "何らかの形でネタを絡めてください(無関係でも強引に繋げてOK)" ) try: messages = [{"role": "user", "content": prompt}] tip = await complete(messages, model=USELESS_TIPS_MODEL) content = f"💡 **今日のお役立ち情報**\n{tip.strip()}" await send_discord_message(DISCORD_WEBHOOK_URL_TIPS, content) except Exception: logger.exception("Failed to send useless tip") def start_scheduler() -> None: scheduler.add_job( send_weather_laundry_notification, trigger=CronTrigger(hour=WEATHER_NOTIFY_HOUR, minute=WEATHER_NOTIFY_MINUTE), id="weather_laundry_notification", replace_existing=True, ) logger.info( "Scheduler started: weather notification at %02d:%02d", WEATHER_NOTIFY_HOUR, WEATHER_NOTIFY_MINUTE, ) if USELESS_TIPS_ENABLED: scheduler.add_job( send_useless_tip, trigger=IntervalTrigger(hours=USELESS_TIPS_INTERVAL_HOURS), id="useless_tips", replace_existing=True, ) logger.info("Useless tips job started: every %d hour(s)", USELESS_TIPS_INTERVAL_HOURS) scheduler.start()