- 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.
7.9 KiB
7.9 KiB
雑談Bot(Discord連携)仕様
docs/architecture.md のチャットアプリ本体に対する拡張機能群。Discord上での雑談応答・天気案内・定期的などうでもいい豆知識配信、およびそれらを支えるタグ付け/ワードプール/知識ベース機構をまとめる。
全体像
Discord ⇄ discord_bot.py ⇄ chat.py (REST/SSE) ⇄ LiteLLM
│ │
├─ scheduler.py ├─ tagging.py(メッセージへタグ付け)
│ (定期実行) ├─ word_memory.py(話題語彙プール)
│ └─ knowledge.py / websearch.py(Web検索→知識ベース)
└─ JMA天気API
1. Discord bot(backend/app/core/discord_bot.py)
- Bot宛てメンション(
on_message)に応答。メンション以外は無視 - 天気キーワード検出時: 気象庁(JMA) APIと連携し天気・洗濯指数を案内
- 雑談キーワード検出時:
- ユーザー発言をチャット応答へリダイレクト(
_generate_useless_talk_redirect) tagging.generate_tags()で発言からタグを3つ生成tagging.find_related_tag()で関連トピックを1つ選定scheduler.send_useless_tip(topic_override=related_tag)を即時実行し、その話題に絡めた豆知識を配信
- ユーザー発言をチャット応答へリダイレクト(
- 受信メッセージは
word_memory.record_message()で語彙プールにも記録される
2. スケジューラ(backend/app/core/scheduler.py)
APSchedulerによる定期ジョブ2種:
| ジョブ | 頻度 | 内容 |
|---|---|---|
| 洗濯天気通知 | 毎日22時 | JMA天気を基に翌日の洗濯指数を配信 |
どうでもいい豆知識 (send_useless_tip) |
USELESS_TIPS_INTERVAL_HOURS時間ごと(既定3h、USELESS_TIPS_ENABLEDで有効化) |
ランダム/指定トピックでLLMに60文字以内の「どうでもいい豆知識」を生成させ配信 |
send_useless_tip(topic_override=None):
- AM3時台は配信スキップ(静音時間)
topic_word = topic_override or word_memory.pick_random_word()でトピックを決定topic_wordが決まれば、プロンプトに「〇〇という話題にこじつけてネタを絡めて」という指示を追加- 生成文を
💡 今日のお役立ち情報として Discord Webhook へ送信
3. メッセージタグ付け(backend/app/core/tagging.py)
generate_tags(content): LLMでメッセージ本文から最大3タグを生成(JSON出力を期待し、崩れていればカンマ区切りにフォールバック)tag_message(db, message): 1メッセージにタグを生成しMessageTagテーブルへ保存。ロール非依存で、ユーザー発言・アシスタント応答の両方に使われるtag_message_by_id(message_id): バックグラウンドタスク用。chat.pyのSSE応答処理内で、ユーザーメッセージ保存後・アシスタント応答保存後の両方でasyncio.create_task()経由で呼ばれる(応答ストリームをブロックしない)tag_memory(db, memory): 要約(Memory)にもタグ付け可能(MemoryTagテーブル)backfill_tags(db, limit=None): 未タグのuser/assistantメッセージを一括タグ付けする保守用関数
find_related_tag(seed_tags) — 関連トピック選定とフォールバックチェーン
雑談リダイレクト時に呼ばれ、最終的に「豆知識のネタ」となる文字列を返す。
seed_tagsが空 →Noneを返す(呼び出し元のsend_useless_tip側でword_memory.pick_random_word()にフォールバック)seed_tagsと同じメッセージ/要約に付いている他のタグ(=関連タグ)を探す。見つかればその中からランダムに1つ選択。見つからなければseed_tags自体からランダムに1つ選択 →chosen_tag_pick_word_for_tag(db, chosen_tag)で、chosen_tagが付いたMessage.content/Memory.summaryの本文から名詞を抽出(word_memory.extract_words)し、現在のワードプールと重なる単語を優先選択(word_memory.pick_word_near)。重なりが無ければ抽出語からランダム選択- 本文から単語が1つも抽出できなければ
chosen_tag(タグ文字列そのもの)を返す
→ 「関連タグが見つかった場合でも、ただタグ文字列を使うのではなく、その話題の実際の発言内容とワードプールの今ホットな語彙を突き合わせて、より自然な単語をネタに選ぶ」という多段フォールバック構成。
4. ワードプール(backend/app/core/word_memory.py)
- Janomeで形態素解析し、名詞(一般・固有名詞・サ変接続)のみを抽出。2文字未満・ストップワードは除外
- Markdownの記法(コードブロック・見出し・リンク等)は事前に除去
_recent_words: deque(maxlen=200)がインメモリの揮発性プール(DB非永続、再起動でリセット。起動時はword_memory.seed_from_history()で直近メッセージから再構築)- 主な関数:
record_message(content): 本文を解析しプールに追記extract_words(content): プールに追記せず単語リストのみ返す(タグ紐づけ用本文の解析に利用)pick_random_word(): プールからランダムに1語pick_word_near(words): 渡した単語群とプールの積集合からランダムに1語(無ければNone)get_word_pool_with_counts(): 出現回数つきでプール全体を返す(WordPool.vue表示用)
chat.pyのSSE処理・discord_bot.pyのメッセージ受信、双方でrecord_message()が呼ばれ語彙が蓄積される
5. 知識ベース/Web検索(websearch.py / knowledge.py)
websearch.py: Tavily APIで検索を実行knowledge.py: 検索結果(クエリ・要約・出典URL)をKnowledgeEntry/KnowledgeSourceテーブルへ永続化し、以後の会話コンテキストへ再注入できる知識ベースとして機能chat.pyのPOST /chat/stream内で、LLMが追加情報を必要とした場合(llm.resolve_web_search)に検索が走り、結果はsourcesイベントとしてフロントへ、要約はknowledge.save_knowledge()でDBへ保存される
6. DBスキーマ(拡張分)
backend/app/db/models.py で追加されたテーブル:
| テーブル | 主なカラム | 用途 |
|---|---|---|
message_tags |
id, message_id (FK), tag, created_at | メッセージ単位のタグ |
memory_tags |
id, memory_id (FK), tag, created_at | 要約(長期メモリ)単位のタグ |
knowledge_entries |
id, query, summary, created_at | Web検索結果の要約 |
knowledge_sources |
id, entry_id (FK), title, url | 知識エントリの出典 |
7. 環境変数(追加分、backend/.env.example)
| 変数 | 説明 |
|---|---|
USELESS_TIPS_ENABLED |
どうでもいい豆知識ジョブの有効化フラグ |
USELESS_TIPS_INTERVAL_HOURS |
豆知識配信の間隔(時間、既定3) |
USELESS_TIPS_MODEL |
豆知識生成に使うLLMモデル(未指定時はDEFAULT_MODEL) |
TAGGING_MODEL |
タグ生成に使うLLMモデル |
| Discord Bot Token / Webhook URL 系 | Discord連携用 |
| 天気API関連 | JMA連携用 |
| Tavily APIキー | Web検索用 |
| Bedrock関連 | LiteLLM経由でのAWS Bedrock利用設定 |
8. フロントエンド: WordPool(frontend/src/components/WordPool.vue)
word_memory.get_word_pool_with_counts()を参照し、現在の話題語彙をタグクラウド的に表示App.vueのサイドバーにSessionList/MemoryMonitorと並んで配置
最終更新: 2026-06-20(タグ付けの双方向化・find_related_tagへのword_memory連携を反映、コミット 234ebba)