# 雑談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と連携し天気・洗濯指数を案内 - **雑談キーワード**検出時: 1. ユーザー発言をチャット応答へリダイレクト(`_generate_useless_talk_redirect`) 2. `tagging.generate_tags()` で発言からタグを3つ生成 3. `tagging.find_related_tag()` で関連トピックを1つ選定 4. `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)` — 関連トピック選定とフォールバックチェーン 雑談リダイレクト時に呼ばれ、最終的に「豆知識のネタ」となる文字列を返す。 1. `seed_tags`が空 → `None` を返す(呼び出し元の`send_useless_tip`側で`word_memory.pick_random_word()`にフォールバック) 2. `seed_tags`と同じメッセージ/要約に付いている他のタグ(=関連タグ)を探す。見つかればその中からランダムに1つ選択。見つからなければ`seed_tags`自体からランダムに1つ選択 → `chosen_tag` 3. `_pick_word_for_tag(db, chosen_tag)` で、`chosen_tag`が付いた `Message.content` / `Memory.summary` の本文から名詞を抽出(`word_memory.extract_words`)し、現在のワードプールと重なる単語を優先選択(`word_memory.pick_word_near`)。重なりが無ければ抽出語からランダム選択 4. 本文から単語が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`)