Add shared notes with tabbed pages editable by user and AI

Adds a note_pages table backing a tabbed notes panel where the user and
the model can both read and write. Only page titles (plus a short
preview) are injected into the system prompt on every turn; full page
content is kept out of context and fetched on demand via a read_note
tool, with write_note letting the model append to, replace, or create a
page.

Generalizes llm.resolve_web_search into resolve_tool_calls so the
tool-calling loop handles web_search and the new note tools through one
path. The chat stream emits a notes_updated SSE event when the model
edits a note so the frontend panel can refresh; local edits are tracked
as dirty and never clobbered by that refresh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 01:27:31 +09:00
co-authored by Claude Opus 5
parent ef588d99d8
commit a08b1785a4
16 changed files with 760 additions and 26 deletions
+14 -3
View File
@@ -59,8 +59,8 @@ Copy-Item .env.example .env
frontend (Vue3 + Pinia, :5173)
↕ REST + SSE (Vite proxy)
backend (FastAPI, :8000)
├── app/api/ チャット・メモリ・ログ のルーター
├── app/core/ LLM ラッパー・メモリロジック・設定
├── app/api/ チャット・メモリ・ノート・ログ のルーター
├── app/core/ LLM ラッパー・メモリロジック・共有ノート・設定
└── app/db/ SQLAlchemy モデル・DB 接続
↕
SQLite (playchat.db) + LiteLLM → 任意の LLM
@@ -71,9 +71,19 @@ SQLite (playchat.db) + LiteLLM → 任意の LLM
- **短期**: セッション内の全メッセージをコンテキストに含める
- **長期**: メッセージ数が `MEMORY_SUMMARIZE_THRESHOLD`(デフォルト 20)を超えると古い半分を LLM で要約 → `memories` テーブルに保存 → 次回以降のシステムプロンプトに注入
### 共有ノート
ユーザーと AI が共同編集するノート。`note_pages` テーブルに 1 ページ 1 行で保存し、UI ではタブとして切り替える。
- **モデルから見える範囲**: システムプロンプトに入るのは**ページのタイトル一覧**(と本文の先頭 40 文字程度のプレビュー)だけ。本文全体は含めない
- **モデルによる読み書き**: `read_note` / `write_note` ツール(`app/core/notes.py`)で行う。`write_note` は存在しないタイトルを指定するとページを新規作成し、`mode` が `append`(既定)なら追記、`replace` なら全置換
- **ツール実行の流れ**: `llm.resolve_tool_calls()` が本番の応答生成前にツール呼び出しを解決する。`web_search` と共通の仕組みで、ノート系ツールは `db` が渡されたときだけモデルに提示される
- **UI への反映**: AI がノートを書き換えた場合、SSE で `notes_updated` を送ってフロント側のノートパネルを再読み込みさせる
- **編集の衝突回避**: フロントは未保存の編集があるページ(`dirtyIds`)をサーバーの値で上書きしない
### SSE ストリーミング
`POST /chat/stream` が SSE を返す。イベント種別: `session_id`(新規セッション ID)/ `chunk`(テキスト断片)/ `done`(完了)/ `error`(エラー)。
`POST /chat/stream` が SSE を返す。イベント種別: `session_id`(新規セッション ID)/ `chunk`(テキスト断片)/ `sources`(Web 検索の出典)/ `notes_updated`(AI が共有ノートを更新)/ `done`(完了)/ `error`(エラー)。
フロントは `src/api/client.ts` の `streamChat()` で fetch + ReadableStream により受信し、`messages.value[msgIdx]` を reactive proxy 経由で更新する。
### Markdown レンダリング
@@ -107,5 +117,6 @@ SQLite (playchat.db) + LiteLLM → 任意の LLM
| `OPENAI_API_KEY` | — | OpenAI API キー |
| `ANTHROPIC_API_KEY` | — | Anthropic API キー |
| `OLLAMA_API_BASE` | `http://localhost:11434` | Ollama エンドポイント |
| `NOTES_TOOL_CONTENT_LIMIT` | `4000` | `read_note` が 1 回に返す本文の最大文字数 |
| `SITE_PASSWORD` | — | サイト全体を保護する共有パスワード(未設定なら無認証) |
| `SESSION_SECRET_KEY` | 起動毎に自動生成 | ログインセッション Cookie の署名鍵 |