Add system memory monitoring, Bedrock support, and Discord integrations

- Add CPU/GPU memory monitoring endpoint and frontend widget
- Add Amazon Bedrock model support (API key / inference profiles) plus model selector entries
- Add Discord weather/laundry notification job (JMA forecast + LLM-generated message)
- Add Discord bot that replies to mentions via the configured LLM, with weather-aware context
- Add scheduled "useless tips" job with separate webhook and quiet-hours skip
- Switch FastAPI startup to lifespan handler, log viewer, markdown rendering, and toast UI additions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 00:59:38 +09:00
co-authored by Claude Sonnet 4.6
parent 903d34761c
commit 4a08ba29fa
34 changed files with 5051 additions and 160 deletions
+127
View File
@@ -0,0 +1,127 @@
"""毎日決まった時刻に天気予報と洗濯指数を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
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 "不明"
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() -> None:
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
prompt = (
"誰の役にも立たない、死ぬほどどうでもいい雑学・豆知識を一つ教えてください。\n"
"- 1〜3文程度で簡潔に\n"
"- 「どうでもいい」感が伝わるようなノリで\n"
"- 絵文字を1つくらい添えてもよい\n"
"- 出力は本文のみ(前置き・説明不要)\n"
"- 過去に何度も言われていそうな定番ネタは避け、できるだけ意外性のある内容にする"
)
try:
messages = [{"role": "user", "content": prompt}]
tip = await complete(messages, model=USELESS_TIPS_MODEL)
content = f"🤷 **どうでもいいtips**\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()