- 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.
112 lines
7.9 KiB
Markdown
112 lines
7.9 KiB
Markdown
# 雑談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`)
|