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:
2026-06-21 11:44:11 +09:00
parent 234ebba103
commit 347230e3c8
29 changed files with 825 additions and 31 deletions
+111
View File
@@ -0,0 +1,111 @@
# 雑談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`)