Schedule reminders by mentioning the Discord bot

メンション本文にリマインド系のキーワードが含まれていたとき、LLM に
create_reminder / list_reminders / cancel_reminder の3ツールを渡し、
自然言語から日時と用件を取り出して reminders テーブルに保存する。
system メッセージに現在時刻を入れてあるので『明日』『30分後』のような
相対表現はモデル側が絶対日時に直す。扱うのは一回きりの通知のみ。

配信は APScheduler の discord_reminders ジョブ(既定30秒間隔)が担い、
Webhook ではなく Bot クライアントから登録元チャンネルへ依頼者を
メンションして送るため、専用の Webhook URL 設定は要らない。

- 日時は他テーブルと同じく UTC で保存し、ユーザー・LLM とのやり取りは
  ローカル時刻で行って reminders.py の境界で変換する
- 過去・REMINDER_MAX_AHEAD_DAYS より先・解釈不能な日時はツール結果として
  エラー文をモデルに返し、聞き直させる
- 一覧・取り消しは依頼者の user_id で絞るため他人のリマインドには触れない
- 配信に5回失敗したら notified_at を立てて諦める(チャンネル削除などで
  ポーリングのたびにエラーログを出し続けるのを防ぐ)。カウンタは
  プロセス内のみなので、再起動すればまた試す

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 23:29:55 +09:00
co-authored by Claude Opus 5
parent 3b30dab650
commit c1aba42cf8
8 changed files with 499 additions and 0 deletions
+12
View File
@@ -103,6 +103,14 @@ LLM に渡すコンテキストは `app/core/memory_kb/memory.py` の `get_conte
- **ダウンロード**: `GET /api/notes/{page_id}/export` が 1 ページを Markdown、`GET /api/notes/export` が全ページを ZIP(1 ページ 1 Markdown、セクションはフォルダ、ファイル名の通し番号で `position` 順を保持)で返す。ファイル名はサーバーが決めるのでフロントは `download` 属性を付けず `Content-Disposition`(日本語タイトルのため RFC 5987 を併記)に任せる
- **セクションによるグルーピング**: `note_pages.section` にセクション名を持たせ、同じセクションのページは `position` 上で常に連続させる(`core/notes.set_section()` が割り当て時に並べ替える)。タブバーは `position` 順に描画し、隣接して同じ `section` が続く区間を1グループとして表示する。`get_notes_context()` はセクションごとに `## セクション名` の見出しを付けてタイトル一覧を出す。セクション名の一括変更は `POST /api/notes/sections/rename`(`core/notes.rename_section()`)
### Discord Bot
`main.py` の lifespan が Bot(メンション応答)と APScheduler(定期ジョブ)を起動する。詳細は `docs/architecture.md`。
- **メンション応答** (`discord/discord_bot.py`): 本文のキーワードで **リマインド → 雑談 → 天気** の順に分岐し、先にマッチしたものが返信する
- **定期ジョブ** (`discord/scheduler.py`): 天気通知(Cron)・どうでもいいTips(Interval)・リマインド配信(Interval)の3つ
- **リマインド** (`discord/reminders.py`): メンションで自然言語登録する**一回きり**の通知(繰り返し非対応)。LLM に `create_reminder` / `list_reminders` / `cancel_reminder` の3ツールを渡し、system メッセージに入れた現在時刻を基準に相対表現を絶対日時へ直させる。**tool calling 対応モデルが必須**。配信は Webhook ではなく Bot クライアントから登録元チャンネルへ依頼者をメンションして送るので、専用の Webhook URL 設定は不要。DB (`reminders`) は UTC で持ち、ユーザー・LLM とのやり取りはローカル時刻に直す。一覧・取り消しは依頼者の user_id で絞るため他人のリマインドには触れられない
### SSE ストリーミング
`POST /chat/stream` が SSE を返す。イベント種別: `session_id`(新規セッション ID)/ `chunk`(テキスト断片)/ `tool`(ツール実行の開始・完了)/ `sources`(Web 検索の出典)/ `notes_updated`(AI が共有ノートを更新)/ `done`(完了)/ `error`(エラー)。
@@ -154,5 +162,9 @@ LLM に渡すコンテキストは `app/core/memory_kb/memory.py` の `get_conte
| `NOTES_IMPORT_MAX_PAGES` | `100` | ノート取り込みで一度に作れるページ数の上限 |
| `NOTES_IMPORT_MAX_BYTES` | `2000000` | 取り込む 1 ファイル(ZIP 内は 1 エントリ)あたりの最大バイト数 |
| `KNOWLEDGE_TOOL_CONTENT_LIMIT` | `4000` | `read_knowledge` が 1 回に返す本文の最大文字数 |
| `REMINDER_CHECK_INTERVAL_SECONDS` | `30` | 期限切れリマインドを探す間隔(秒) |
| `REMINDER_MAX_AHEAD_DAYS` | `365` | これより先の日時のリマインドは登録を拒否する |
| `REMINDER_LIST_LIMIT` | `20` | `list_reminders` が一度に返す件数 |
| `REMINDER_MODEL` | — | リマインドの日時解釈に使うモデル(未設定なら `WEATHER_NOTIFY_MODEL` → `DEFAULT_MODEL`。tool calling 対応が必須) |
| `SITE_PASSWORD` | — | サイト全体を保護する共有パスワード(未設定なら無認証) |
| `SESSION_SECRET_KEY` | 起動毎に自動生成 | ログインセッション Cookie の署名鍵 |