Files
colcaroli 347230e3c8 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.
2026-06-21 11:44:11 +09:00

7.9 KiB
Raw Permalink Blame History

雑談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メッセージを一括タグ付けする保守用関数

雑談リダイレクト時に呼ばれ、最終的に「豆知識のネタ」となる文字列を返す。

  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)