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
+84
View File
@@ -0,0 +1,84 @@
"""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.weather import fetch_tomorrow_weather, judge_laundry
logger = logging.getLogger(__name__)
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
WEATHER_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 "不明"
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:
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 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")