Reorganize core into subpackages, add local KB, ruff/pyright tooling
- Split backend/app/core into weather/, discord/, memory_kb/, search/ subpackages by responsibility, keeping config.py and llm.py as the shared foundation; updated all import sites accordingly. - Add local Markdown knowledge base support (local_knowledge.py, knowledge_base/fgo.md, LOCAL_KB_* settings) that embeds and injects relevant docs into the system prompt by topic similarity. - Add ruff and pyright as backend dev dependencies with basic config in pyproject.toml for linting and type checking. - Document local machine quirks in CLAUDE.md (Postgres on port 8080, SSLKEYLOGFILE breaking litellm/pyright via OPENSSL_Uplink crashes). - Add backend/run_dev.ps1 dev launcher and docs/discord-bot.md.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"""毎日決まった時刻に天気予報と洗濯指数を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()
|
||||
Reference in New Issue
Block a user