Files
playchat/docs/features.md
colcaroliandClaude Opus 5 f085db421c Browse the knowledge base from a single side panel menu
Add a read-only view of backend/knowledge_base/**.md so the files that get
injected into the system prompt can be inspected from the GUI:

- GET /api/kb/files lists the Markdown files (title, preview, size, mtime and
  whether the file is already embedded), GET /api/kb/file returns one in full.
  Paths are resolved against the knowledge base directory and rejected unless
  they stay inside it and end in .md.
- KnowledgeBasePanel.vue groups the files by folder (collapsible, so web_search
  can be folded away), filters by title/path/preview and renders the selected
  file with MarkdownContent.

The header had grown to four side panel toggles, so collapse them into one
menu: panels.ts now holds the list of panels along with the component each one
renders, App.vue picks it with <component :is>, and SidePanelMenu.vue shows the
active panel on its button. Re-picking the open panel (or "パネルを閉じる")
closes it; a click outside or Esc closes just the menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 21:38:54 +09:00

218 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 機能一覧
このリポジトリにあるアプリ(PlayChat)が「何ができるか」を機能単位でまとめたもの。
内部設計は [architecture.md](architecture.md)、エンドポイント仕様は [api.md](api.md)、
Discord 連携は [discord-bot.md](discord-bot.md)、ノートの操作方法は [notes-guide.md](notes-guide.md)、
デプロイ手順は [deploy-lightsail.md](deploy-lightsail.md) を参照。
## これは何か
個人用の LLM チャットアプリ。FastAPI(Python)+ Vue 3(TypeScript)で、会話・記憶・ノートを
すべて手元の SQLite に持つ。LLM は LiteLLM 経由なので OpenAI / Anthropic / Ollama / Amazon Bedrock を
同じ画面のプルダウンで切り替えられる。単なるチャット UI ではなく、
- 会話が長くなったら勝手に要約して長期記憶に落とす
- 知らないことは自分で Web 検索して、その結果を次回以降も使える知識として溜める
- ユーザーと AI が同じノートを共同編集する
- Discord 側にも住んでいて、天気を知らせたり雑談に乗ったりする
といった「動き続ける」部分を持つのが特徴。
## 機能サマリ
| # | 機能 | 概要 | 主な実装 |
|---|---|---|---|
| 1 | チャット(SSE ストリーミング) | 逐次表示・Markdown 描画・セッション管理 | `api/chat.py` / `ChatWindow.vue` |
| 2 | セッションの書き出し / 取り込み | 会話を JSON でダウンロード・復元 | `api/chat.py` / `SessionList.vue` |
| 3 | 5 層コンテキスト管理 | 蓄積知識・ノート・ローカル KB・長期/短期メモリを毎回合成 | `core/memory_kb/memory.py` |
| 4 | 自動要約による長期記憶 | 一定件数を超えたら古い会話を要約して圧縮 | `memory_kb/memory.py` |
| 5 | Web 検索と知識の蓄積 | Tavily で検索 →要約 → DB とローカル KB に保存 | `core/search/` / `memory_kb/knowledge.py` |
| 6 | ローカル Markdown ナレッジベース | 話題に近いファイルを埋め込み類似度で選んで全文注入 | `memory_kb/local_knowledge.py` |
| 6.5 | ナレッジベース閲覧パネル | KB の Markdown をサイドパネルで一覧・全文表示 | `api/knowledge_base.py` / `KnowledgeBasePanel.vue` |
| 7 | 共有ノート | タブ/セクション付きノートを人と AI の両方が編集 | `core/notes.py` / `NotePanel.vue` |
| 8 | 自動タグ付け | 発言・要約にタグを 3 つ付与(話題選びに使う) | `memory_kb/tagging.py` |
| 9 | ワードプール | 直近の名詞をインメモリに保持しタグクラウド表示 | `memory_kb/word_memory.py` / `WordPool.vue` |
| 10 | Discord Bot | メンションに返信。天気・雑談キーワードに反応 | `core/discord/discord_bot.py` |
| 11 | 定期通知 | 毎日の洗濯天気通知と、定期的な「どうでもいい豆知識」 | `core/discord/scheduler.py` |
| 12 | 天気予報(気象庁 API) | 明日の天気・降水確率から洗濯指数を判定 | `core/weather/` / `Clock.vue` |
| 13 | モデル切替とプリセット管理 | 選択肢を DB で管理し画面から追加・編集・削除 | `api/models.py` / `SettingsPanel.vue` |
| 14 | GUI 設定画面 | API キー等を再起動なしで変更(許可リスト制・マスク表示) | `core/settings_store.py` |
| 15 | API ログビューア | LLM 呼び出しの成功/失敗・レイテンシ・リクエスト全文 | `api/logs.py` / `LogViewer.vue` |
| 16 | システムモニタ | CPU・メモリ・GPU 使用率を 10 秒ごとに表示 | `api/system.py` / `MemoryMonitor.vue` |
| 17 | サイト認証 | 共有パスワード 1 つでサイト全体を保護 | `core/site_auth.py` / `LoginGate.vue` |
| 17.5 | 右サイドパネルのメニュー | ノート/ナレッジベース/設定/ログを 1 つのメニューから切替 | `panels.ts` / `SidePanelMenu.vue` |
| 18 | Docker 一式 | Caddy + nginx + FastAPI で HTTPS 公開 | `docker-compose.yml` / `Caddyfile` |
---
## 1. チャット
- `POST /chat/stream` が SSE を返し、フロントは fetch + ReadableStream で受けて 1 文字ずつ描画する。
イベントは `session_id` / `chunk` / `tool` / `sources` / `notes_updated` / `done` / `error` の 7 種。
- アシスタントの発言は `marked` + `DOMPurify` で描画する。ストリーミング途中で閉じていないコードフェンスは
自動で補完してから描画するので、生成中でもレイアウトが崩れない。
- セッションは左サイドバーの一覧から切り替え・削除できる。タイトルは最初の発言の先頭 40 文字。
- Web 検索が走った応答には出典(`sources`)が、各メッセージには自動生成されたタグが添えて表示される。
- Web 検索・ノートの読み書き・蓄積知識の読み出しといったツール実行は、バブルの上に
「🔍 Web検索 / クエリ / ✓ 5件の結果」の形でリアルタイムに表示される(`ToolActivityList.vue`)。
実行中はスピナーが回り、応答が途中で切れたツールは「⚠ 中断されました」になる。表示専用で永続化はしない。
- 失敗時は赤いバブルとトースト通知の両方で知らせる。
## 2. セッションの書き出し / 取り込み
- `GET /chat/sessions/{id}/export` がセッションのメタ情報と全メッセージ(タグ込み)を JSON で返す。
- `POST /chat/sessions/import` は同じ形式の JSON から**新しいセッションを作成**する(上書きではない)。
サイドバーの ⭱ ボタンからファイルを選ぶだけで取り込める。
- 要約済み(アーカイブ済み)のメッセージも `archived_at` 付きでそのまま書き出され、取り込み時もアーカイブ状態を保つ。
## 3. コンテキスト管理(5 層)
毎リクエスト `get_context_messages()` が以下の順に system メッセージを積み、最後に会話履歴を連ねる。
| 層 | 中身 | 量の制御 |
|---|---|---|
| 1 蓄積知識 | 過去の Web 検索結果の**見出し一覧のみ**(本文は `read_knowledge` で読む) | 最新 `KNOWLEDGE_CONTEXT_LIMIT`(既定 15)件 |
| 2 共有ノート | ページのタイトル一覧+本文先頭 40 文字 | 全ページ(本文は入れない) |
| 2.5 参照指定ノート | このチャットに紐付けたページの**本文全文** | 紐付いたページのみ・1 ページ `SESSION_NOTES_CONTENT_LIMIT`(既定 8000)文字 |
| 3 ローカル KB | 選ばれた Markdown の**全文** | 類似度上位 `LOCAL_KB_TOP_K`(既定 3)件かつ `LOCAL_KB_MIN_SCORE`(既定 0.75)以上 |
| 4 長期メモリ | 要約を全件連結 | 上限なし |
| 5 短期メモリ | セッション内の全メッセージ | 上限なし(要約で間接的に制御) |
層 2.5 だけは例外で、ユーザーが 📌 で選んだページの本文をツールを介さず直接載せる。
重い情報(ノート本文・蓄積知識の本文・Web 検索)は目次だけを渡し、本体はツール(`read_note` / `write_note` / `read_knowledge` / `web_search`)で
取りに行かせる遅延読み込み方式。`llm.resolve_tool_calls()` が本番生成の前に最大 2 ラウンドでこれを解決する。
## 4. 自動要約による長期記憶
- メッセージ数が `MEMORY_SUMMARIZE_THRESHOLD`(既定 20)を超えると、古い側の半分を LLM が要約して
`memories` に 1 行として保存する。
- **要約した元メッセージはアーカイブされる**(物理削除はしない)。LLM に渡るコンテキストからは外れるが、チャット画面では薄く表示され、区切り線から先だけがモデルに渡ることが分かるようになっている。
- 保存した要約にもタグが付き、Discord Bot の話題選びの材料になる。
- 溜まった要約は `GET /memory/{session_id}` で確認、`DELETE /memory/{id}` で個別削除できる(API のみ。専用の画面は無い)。
## 5. Web 検索と知識の蓄積
- LLM が必要と判断したときだけ `web_search` ツールが走る(Tavily。`TAVILY_API_KEY` 未設定なら空振り)。
- 結果は `SEARCH_SUMMARY_MODEL` で構造化要約にされ、**2 か所**に保存される。
- `knowledge_entries` / `knowledge_sources` テーブル → 次回以降、層 1 として全セッションに効く
- `knowledge_base/web_search/*.md` → 層 3 の類似度検索の対象になる
- `POST /chat/knowledge/regenerate` で、既存の知識エントリを出典 URL から取り直して要約し直せる。
## 6. ローカル Markdown ナレッジベース
- `backend/knowledge_base/**.md` を置いておくだけで、話題が近いときに自動で全文が注入される。
- 直近のユーザー発言との埋め込みコサイン類似度で選ぶ。埋め込みは `local_knowledge_docs` に
内容ハッシュ付きでキャッシュされ、ファイルが変わったときだけ取り直す。
- プロンプト上は「Web 検索結果より優先。これで答えられるなら検索するな」と明示している。
- ヘッダーのパネルメニュー →「ナレッジベース」で開くサイドパネル(`KnowledgeBasePanel.vue`)から中身を閲覧できる。
フォルダ単位のグループ表示・折りたたみ、タイトル/パス/本文先頭での絞り込み、Markdown 描画に対応。
各ファイルの緑の点は埋め込み済み(=自動注入の対象)であることを示す。閲覧専用で、編集はできない。
## 7. 共有ノート
- 1 ページ=1 タブ。作成・改名・削除・本文編集(入力が止まると自動保存)ができる。
- **セクション**でページをグループ化でき、同じセクションのタブは常に隣り合って表示される。
- タブは**ドラッグ&ドロップ**で並べ替えでき、他セクションのタブにドロップすればそのまま移動になる。
- AI 側はタイトル一覧だけを常に見ており、`read_note` で本文を読み、`write_note` で追記/全置換/新規作成する。
AI が書き換えると SSE の `notes_updated` が飛び、開いているノートパネルが自動で最新化される。
- 自分が編集中(未保存)のページは AI の更新で上書きされない。
- **取り込み**: `.md` / `.txt` / `.zip` を本文エリアにドロップするか ⬆ ボタンで選ぶとページになる。
ZIP はフォルダ名がセクションになるので、⬇ で出した ZIP をそのまま戻せば構成ごと復元される。
タイトルが衝突したら `(2)` を足して別ページにするので、既存のページは壊れない。
- **チャットへの紐付け**: タブの 📌 を押すと、そのページの本文が**今開いているチャット**のシステム
プロンプトに毎回そのまま載る。AI が `read_note` を呼ぶのを待たずに確実に参照させたいときに使う。
指定はチャット単位で保存されるので、会話ごとに違う組み合わせを選べる。
- **ダウンロード**: パネル下部の ⬇ ボタンで、開いているページを `.md` として、または全ページを
ZIP(1 ページ 1 Markdown、セクションはフォルダ、ファイル名の通し番号でタブ順を保持)として
手元に保存できる。未保存の編集は書き出す前に保存される。
## 8-9. 自動タグ付けとワードプール
- 各メッセージと各要約に、バックグラウンドでタグが最大 3 つ付く(応答ストリームはブロックしない)。
タグはコンテキストには注入せず、Discord Bot が「近い話題」を選ぶために使う。
- `POST /api/system/tags/backfill` で、過去の未タグ分をまとめて処理できる。
- ワードプールは Janome で抽出した名詞を `deque(maxlen=200)` にプロセス内保持するもの。
**DB に永続化されず再起動で消える**が、起動時に直近 200 件のメッセージから再シードされる。
サイドバーに出現回数に応じた大きさのタグクラウドとして表示される。
## 10-11. Discord Bot と定期通知
- Bot はメンションされたときだけ反応する。本文に天気系キーワード(天気・洗濯・雨など)が含まれれば
気象庁データを添えて回答し、雑談系キーワード(どうでもいい・暇など)なら発言からタグを起こして
関連する話題の豆知識を即座に投げ返す。
- APScheduler による定期ジョブが 2 つ。
- **洗濯天気通知**: 毎日 `WEATHER_NOTIFY_HOUR` 時(既定 22 時)に、翌日の天気と洗濯指数を配信
- **どうでもいい豆知識**: `USELESS_TIPS_ENABLED=true` のときだけ登録され、`USELESS_TIPS_INTERVAL_HOURS`(既定 3 時間)ごとに実行。深夜 3 時台はスキップ
- どちらも `POST /api/system/weather-notify/test` / `POST /api/system/useless-tip/test` で即時実行して確認できる。
## 12. 天気予報
- 気象庁の無料 JSON API から明日の予報を取り、天気文言と降水確率から洗濯指数を判定する
(雨雪なら室内干し、降水確率 30% 以下なら外干し OK、それ以上は室内干し推奨)。
- 取得した最新データはメモリ上にキャッシュされ、画面右上の時計の横にも表示される(`GET /api/system/weather/latest`)。
- 対象地域は `WEATHER_AREA_CODE`(既定 120000=千葉県)と `WEATHER_AREA_KEYWORD`(既定「北西部」)で指定する。
## 13-14. モデル選択と設定画面
- モデルのプルダウンは `model_presets` テーブル由来で、初回起動時に既定の一覧がシードされる。
設定画面からラベル/モデル文字列を追加・編集・削除できる(重複は 400 で弾く)。
- 設定画面では API キー類を**再起動なしで**変更できる。
- 変更できるのは許可リストにあるキーだけ(`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / AWS 系 /
`OLLAMA_API_BASE` / `TAVILY_API_KEY` / `LOCAL_KB_EMBEDDING_MODEL`)
- 値は末尾 4 文字だけを見せるマスク表示。空欄で保存すると「変更なし」扱い
- 保存すると `app_settings` テーブルに入り `os.environ` に反映される。削除すると `.env` 由来の起動時の値に戻る
## 15-16. ログビューアとシステムモニタ
- LLM 呼び出しは成功・失敗どちらも `api_logs` に記録される(モデル・リクエスト全文・応答・
レイテンシ・エラー)。ログパネルからセッション別/成功失敗別に絞り込み、個別削除ができる。
- サイドバーの監視パネルは CPU 使用率・メモリ使用量を 10 秒ごとに更新する。
`pynvml` が使える環境なら GPU メモリも表示する(無ければその欄が出ないだけ)。
## 17. 認証
パスワードが 2 種類あり、役割が違う。
| 変数 | 守る対象 | 方式 |
|---|---|---|
| `SITE_PASSWORD` | チャット含むサイト全体 | ログイン画面 → HMAC 署名付きセッション Cookie(既定 30 日) |
| `SETTINGS_PASSWORD` | 設定画面と モデルプリセットの更新系のみ | HTTP Basic(リクエストごとに送る) |
- どちらも未設定時の挙動が逆なので注意。`SITE_PASSWORD` 未設定=**誰でも使える**、
`SETTINGS_PASSWORD` 未設定=**設定画面は常に 403**(空パスワードでの突破を防ぐため)。
- `SESSION_SECRET_KEY` を設定しないと起動ごとに鍵が作り直され、再起動のたびに全員ログインし直しになる。
- ローカルの http 環境でログインが効かない場合のみ `SESSION_COOKIE_SECURE=false` にする。
## 17.5. 右サイドパネル(1 つのメニューに集約)
- ノート / ナレッジベース / 設定 / ログは、ヘッダー右端のメニュー 1 つから切り替える。
- 開いているパネルのアイコンと名前がボタンに出る。同じ項目をもう一度選ぶか「パネルを閉じる」で閉じる。
- メニューの外のクリックと `Esc` でメニューだけを閉じられる(パネルは開いたまま)。
- パネルを増やすときは `frontend/src/panels.ts` に 1 行足すだけ。
メニューの項目と、開いたときに描画するコンポーネントをこの配列が兼ねている。
## 18. デプロイ
- `docker-compose.yml` に backend(FastAPI)/ frontend(nginx で静的配信+API リバースプロキシ)/
caddy(Let's Encrypt 自動取得と HTTPS 終端)の 3 コンテナが定義済み。
- SQLite と `knowledge_base` は Docker ボリュームに永続化される。
- フロントは API をすべて相対パスで呼ぶため、同一オリジン配信なら CORS 設定は実質不要。
---
## 制約・注意点
このアプリを触るときに引っかかりやすい点。
- **1 応答あたり LLM 呼び出しは最低 2 回**。ツールを使わない会話でも `resolve_tool_calls()` が
フルコンテキストを 1 回投げてから本番生成に入る。
- **トークン数ベースの制御が一切ない**。すべて件数・文字数ベースなので、層 3(Markdown 全文 ×3)は
会話が短くてもコンテキストを大きく押し上げる(層 1 は見出しのみなので軽い)。
- **長期メモリに上限がない**。長いセッションでは要約が積み上がり続ける(要約の再要約はしない)。
- **要約自体は不可逆**。アーカイブされた元メッセージは残るが、要約をやり直したり元の状態に戻す手段は無い。
- **ワードプールは揮発性**。再起動でリセットされ、直近 200 件から作り直される。
- **DB マイグレーションは簡易実装**。Alembic は依存に入っているが使っておらず、起動時に
`PRAGMA table_info` を見て不足カラムを `ALTER TABLE` で足すだけ(SQLite 前提)。
- **自動テストは無い**。`pytest` は dev 依存に入っているが `backend/tests/` は存在しない。