Files
playchat/backend/app/core/scheduler.py
T
colcaroli 234ebba103 Add web search knowledge base, message tagging, and word-memory tip topics
Introduces websearch.py (Tavily) + knowledge.py to ground chat responses
in search results, tagging.py to tag user and assistant messages via LLM
and pick related topics for the Discord useless-tips job, and
word_memory.py to track recent conversation nouns (surfaced in the new
WordPool.vue sidebar) used to pick tip topics close to a related tag.
2026-06-20 18:06:06 +09:00

148 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""毎日決まった時刻に天気予報と洗濯指数を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.notify import send_discord_message
from app.core.weather import fetch_tomorrow_weather, judge_laundry
from app.core.weather_cache import set_latest
from app.core.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()