Store timestamps as UTC-aware and slim the knowledge context to headlines
タイムゾーン: SQLite の DATETIME は tzinfo を保存できず、DateTime(timezone=True) を 指定してもバインド時に捨てられるため、API のレスポンスがオフセット無しの ISO 文字列になりフロントがローカル時刻として誤解していた。UtcDateTime (TypeDecorator) を追加し、書き込み時は naive な UTC に正規化、読み出し時は UTC の aware として返すようにする。格納フォーマットは変わらないので既存 データはそのまま正しく解釈でき、マイグレーションは不要。 コンテナは TZ 未設定で UTC だったため、天気通知の cron 時刻・深夜スキップ 判定・「明日」の日付計算が9時間ずれていた。TZ=Asia/Tokyo を指定し、slim イメージに無い tzdata を導入する。 蓄積知識: 層1が最新15件の要約全文を毎リクエスト無条件に注入しており、同じ内容が ローカルKB(層3)にも保存されているため重複していた。層1を「クエリ+ 一行要約」の目次に変え、本文は read_knowledge ツールで読み出す形にする。 一行要約は要約生成時の既存 LLM 呼び出しに相乗りするので呼び出し回数は 増えない。headline を持たない既存エントリは本文の先頭行から導出する。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
# 機能一覧
|
||||
|
||||
このリポジトリにあるアプリ(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` |
|
||||
| 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` |
|
||||
| 18 | Docker 一式 | Caddy + nginx + FastAPI で HTTPS 公開 | `docker-compose.yml` / `Caddyfile` |
|
||||
|
||||
---
|
||||
|
||||
## 1. チャット
|
||||
|
||||
- `POST /chat/stream` が SSE を返し、フロントは fetch + ReadableStream で受けて 1 文字ずつ描画する。
|
||||
イベントは `session_id` / `chunk` / `sources` / `notes_updated` / `done` / `error` の 6 種。
|
||||
- アシスタントの発言は `marked` + `DOMPurify` で描画する。ストリーミング途中で閉じていないコードフェンスは
|
||||
自動で補完してから描画するので、生成中でもレイアウトが崩れない。
|
||||
- セッションは左サイドバーの一覧から切り替え・削除できる。タイトルは最初の発言の先頭 40 文字。
|
||||
- Web 検索が走った応答には出典(`sources`)が、各メッセージには自動生成されたタグが添えて表示される。
|
||||
- 失敗時は赤いバブルとトースト通知の両方で知らせる。
|
||||
|
||||
## 2. セッションの書き出し / 取り込み
|
||||
|
||||
- `GET /chat/sessions/{id}/export` がセッションのメタ情報と全メッセージ(タグ込み)を JSON で返す。
|
||||
- `POST /chat/sessions/import` は同じ形式の JSON から**新しいセッションを作成**する(上書きではない)。
|
||||
サイドバーの ⭱ ボタンからファイルを選ぶだけで取り込める。
|
||||
- 注意: 要約済みの会話は元メッセージが物理削除されているため、エクスポートしても生ログは戻らない。
|
||||
|
||||
## 3. コンテキスト管理(5 層)
|
||||
|
||||
毎リクエスト `get_context_messages()` が以下の順に system メッセージを積み、最後に会話履歴を連ねる。
|
||||
|
||||
| 層 | 中身 | 量の制御 |
|
||||
|---|---|---|
|
||||
| 1 蓄積知識 | 過去の Web 検索結果の**見出し一覧のみ**(本文は `read_knowledge` で読む) | 最新 `KNOWLEDGE_CONTEXT_LIMIT`(既定 15)件 |
|
||||
| 2 共有ノート | ページのタイトル一覧+本文先頭 40 文字 | 全ページ(本文は入れない) |
|
||||
| 3 ローカル KB | 選ばれた Markdown の**全文** | 類似度上位 `LOCAL_KB_TOP_K`(既定 3)件かつ `LOCAL_KB_MIN_SCORE`(既定 0.75)以上 |
|
||||
| 4 長期メモリ | 要約を全件連結 | 上限なし |
|
||||
| 5 短期メモリ | セッション内の全メッセージ | 上限なし(要約で間接的に制御) |
|
||||
|
||||
重い情報(ノート本文・蓄積知識の本文・Web 検索)は目次だけを渡し、本体はツール(`read_note` / `write_note` / `read_knowledge` / `web_search`)で
|
||||
取りに行かせる遅延読み込み方式。`llm.resolve_tool_calls()` が本番生成の前に最大 2 ラウンドでこれを解決する。
|
||||
|
||||
## 4. 自動要約による長期記憶
|
||||
|
||||
- メッセージ数が `MEMORY_SUMMARIZE_THRESHOLD`(既定 20)を超えると、古い側の半分を LLM が要約して
|
||||
`memories` に 1 行として保存する。
|
||||
- **要約した元メッセージは DB から物理削除される**(復元不可)。
|
||||
- 保存した要約にもタグが付き、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 検索結果より優先。これで答えられるなら検索するな」と明示している。
|
||||
|
||||
## 7. 共有ノート
|
||||
|
||||
- 1 ページ=1 タブ。作成・改名・削除・本文編集(入力が止まると自動保存)ができる。
|
||||
- **セクション**でページをグループ化でき、同じセクションのタブは常に隣り合って表示される。
|
||||
- タブは**ドラッグ&ドロップ**で並べ替えでき、他セクションのタブにドロップすればそのまま移動になる。
|
||||
- AI 側はタイトル一覧だけを常に見ており、`read_note` で本文を読み、`write_note` で追記/全置換/新規作成する。
|
||||
AI が書き換えると SSE の `notes_updated` が飛び、開いているノートパネルが自動で最新化される。
|
||||
- 自分が編集中(未保存)のページは AI の更新で上書きされない。
|
||||
|
||||
## 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` にする。
|
||||
|
||||
## 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 は見出しのみなので軽い)。
|
||||
- **長期メモリに上限がない**。長いセッションでは要約が積み上がり続ける(要約の再要約はしない)。
|
||||
- **要約は不可逆**。要約対象になった元メッセージは DB から消える。
|
||||
- **ワードプールは揮発性**。再起動でリセットされ、直近 200 件から作り直される。
|
||||
- **DB マイグレーションは簡易実装**。Alembic は依存に入っているが使っておらず、起動時に
|
||||
`PRAGMA table_info` を見て不足カラムを `ALTER TABLE` で足すだけ(SQLite 前提)。
|
||||
- **自動テストは無い**。`pytest` は dev 依存に入っているが `backend/tests/` は存在しない。
|
||||
Reference in New Issue
Block a user