Add section grouping to shared note tabs
Notes can now be clustered into named sections in the tab bar: each tab gets a 🏷 button to assign it to a section, and clicking a section's label renames (or, given an empty name, ungroups) every page in it. note_pages gains a `section` column. Since the app has no migration framework, database.py adds the column to existing SQLite files at startup via PRAGMA table_info + ALTER TABLE. core/notes.set_section() keeps same-section pages contiguous by renumbering position whenever a page's section changes, so the tab bar and the AI-facing title list (now grouped under "## section" headings) can both treat "adjacent" as "same group" without a separate ordering field. Also fixes a bug in the tab rename input: ref="renameInput" sits inside a nested v-for, which Vue 3 binds as an array rather than a single element, so renameInput.value?.select() was throwing on every rename. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+28
-4
@@ -93,9 +93,10 @@ SSE でストリーミング応答を返す。
|
||||
|
||||
ユーザーと AI が共同編集するノート。ページのタイトル一覧は毎回システムプロンプトに注入され、
|
||||
本文は AI が `read_note` / `write_note` ツールで読み書きする。
|
||||
`section` を設定すると、同じセクション名のページはタブバー上で常に連続してグループ表示される。
|
||||
|
||||
### GET /api/notes
|
||||
全ページを `position` 昇順で返す。
|
||||
全ページを `position` 昇順で返す。同じ `section` のページは常に連続する(隣接=同一グループとして扱われる)。
|
||||
|
||||
**レスポンス**
|
||||
```json
|
||||
@@ -106,6 +107,7 @@ SSE でストリーミング応答を返す。
|
||||
"content": "- 牛乳
|
||||
- 卵",
|
||||
"position": 0,
|
||||
"section": "生活",
|
||||
"updated_at": "2026-08-19T12:00:00"
|
||||
}
|
||||
]
|
||||
@@ -115,21 +117,43 @@ SSE でストリーミング応答を返す。
|
||||
|
||||
### POST /api/notes
|
||||
ページを新規作成し、`201 Created` で作成したページを返す。
|
||||
`section` を指定すると、作成直後にそのセクションの末尾へ配置される(他ページの `position` も再採番される)。
|
||||
|
||||
**リクエスト**
|
||||
```json
|
||||
{ "title": "新しいページ", "content": "" }
|
||||
{ "title": "新しいページ", "content": "", "section": null }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PATCH /api/notes/{page_id}
|
||||
ページのタイトル・本文を更新する。指定したフィールドだけが変更される。
|
||||
ページのタイトル・本文・セクションを更新する。指定したフィールドだけが変更される。
|
||||
タイトルを空文字にしようとした場合は `400`、ページが存在しない場合は `404` を返す。
|
||||
|
||||
`section` はリクエストボディに**キーとして含まれているかどうか**で判定する(`null` を明示的に送るとグループ解除)。
|
||||
セクションを変更すると、そのページは新しいセクションの末尾(該当ページが無ければ末尾)へ移動し、
|
||||
全ページの `position` が振り直される。
|
||||
|
||||
**リクエスト**
|
||||
```json
|
||||
{ "content": "更新後の本文" }
|
||||
{ "content": "更新後の本文", "section": "生活" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/notes/sections/rename
|
||||
`old_name` セクションに属する全ページを `new_name` セクションへ一括で付け替える。
|
||||
`new_name` を空文字/未指定にすると、そのセクションの全ページのグループを解除する。
|
||||
ページの並び順(`position`)は変更しない。
|
||||
|
||||
**リクエスト**
|
||||
```json
|
||||
{ "old_name": "生活", "new_name": "生活雑貨" }
|
||||
```
|
||||
|
||||
**レスポンス**
|
||||
```json
|
||||
{ "renamed": 2 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+90
-19
@@ -21,18 +21,35 @@
|
||||
```
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── main.py # FastAPI インスタンス、CORS、ルーター登録、DB初期化
|
||||
│ ├── main.py # FastAPI インスタンス、CORS、ルーター登録、DB初期化、起動時シード
|
||||
│ ├── api/
|
||||
│ │ ├── chat.py # チャット・セッション関連エンドポイント
|
||||
│ │ ├── memory.py # 長期メモリ参照・削除
|
||||
│ │ └── logs.py # API ログ参照・削除
|
||||
│ │ ├── chat.py # チャット・セッション関連エンドポイント(SSE / export / import)
|
||||
│ │ ├── memory.py # 長期メモリ参照・削除
|
||||
│ │ ├── notes.py # 共有ノート CRUD
|
||||
│ │ ├── models.py # モデルプリセット CRUD
|
||||
│ │ ├── settings.py # GUI 設定画面(APIキー等)
|
||||
│ │ ├── system.py # タグ backfill・ワードプール参照などの運用系
|
||||
│ │ ├── auth.py # サイトログイン
|
||||
│ │ └── logs.py # API ログ参照・削除
|
||||
│ ├── core/
|
||||
│ │ ├── config.py # .env 読み込み・定数定義
|
||||
│ │ ├── llm.py # LiteLLM ラッパー(ストリーミング・ログ記録)
|
||||
│ │ └── memory.py # メモリ読み書きロジック(要約・コンテキスト生成)
|
||||
│ │ ├── config.py # .env 読み込み・定数定義
|
||||
│ │ ├── llm.py # LiteLLM ラッパー(ストリーミング・埋め込み・ツール解決・ログ記録)
|
||||
│ │ ├── notes.py # 共有ノート(タイトル一覧の注入 / read_note・write_note ツール)
|
||||
│ │ ├── memory_kb/ # コンテキスト管理まわり一式
|
||||
│ │ │ ├── memory.py # コンテキスト合成・要約(旧 core/memory.py)
|
||||
│ │ │ ├── knowledge.py # Web検索で得た知識の蓄積・要約・再生成
|
||||
│ │ │ ├── local_knowledge.py # ローカル Markdown KB の埋め込み・類似検索
|
||||
│ │ │ ├── tagging.py # メッセージ・要約への自動タグ付け
|
||||
│ │ │ └── word_memory.py # 形態素解析による単語プール(Discord Bot 用)
|
||||
│ │ ├── search/
|
||||
│ │ │ └── websearch.py # Tavily による web_search ツール
|
||||
│ │ ├── discord/ # Discord Bot・定期通知
|
||||
│ │ ├── settings_store.py # GUI 設定を os.environ へ反映
|
||||
│ │ └── auth.py / site_auth.py # 設定画面パスワード・サイトログイン
|
||||
│ └── db/
|
||||
│ ├── database.py # SQLite 接続・セッションファクトリ・テーブル自動作成
|
||||
│ └── models.py # SQLAlchemy モデル定義
|
||||
│ ├── database.py # SQLite 接続・セッションファクトリ・テーブル自動作成
|
||||
│ └── models.py # SQLAlchemy モデル定義
|
||||
├── knowledge_base/ # ローカルナレッジベース(Markdown。web_search/ 配下は自動生成)
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
@@ -42,30 +59,83 @@ backend/
|
||||
|---|---|---|
|
||||
| `sessions` | id (UUID), title, model, created_at, updated_at | チャットセッション |
|
||||
| `messages` | id, session_id, role, content, created_at | 会話メッセージ(短期メモリ) |
|
||||
| `message_tags` | id, message_id, tag, created_at | メッセージへの自動タグ |
|
||||
| `memories` | id, session_id, summary, embedding_json, created_at | 要約による長期メモリ |
|
||||
| `note_pages` | id, title, content, position, created_at, updated_at | ユーザーと AI が共同編集する共有ノートのページ |
|
||||
| `memory_tags` | id, memory_id, tag, created_at | 要約への自動タグ |
|
||||
| `knowledge_entries` | id, query, summary, created_at | Web 検索で得た知識(セッション横断) |
|
||||
| `knowledge_sources` | id, entry_id, title, url | 上記知識の出典 URL |
|
||||
| `local_knowledge_docs` | id, path, title, content_hash, embedding_json, updated_at | ローカル Markdown KB の埋め込みキャッシュ |
|
||||
| `note_pages` | id, title, content, position, section, created_at, updated_at | ユーザーと AI が共同編集する共有ノートのページ(`section` でタブをグループ化) |
|
||||
| `app_settings` | key, value, updated_at | GUI 設定画面から編集した接続情報 |
|
||||
| `model_presets` | id, label, value, created_at | モデル選択プルダウンの選択肢 |
|
||||
| `api_logs` | id, session_id, model, request_json, response_text, error, latency_ms, created_at | LLM API 呼び出しログ |
|
||||
|
||||
### メモリ機構(`core/memory.py`)
|
||||
### コンテキスト管理(`core/memory_kb/`)
|
||||
|
||||
1. **短期メモリ**: セッション内の全 `messages` をコンテキストとして送信
|
||||
2. **長期メモリ**: メッセージ数が `MEMORY_SUMMARIZE_THRESHOLD`(デフォルト 20)を超えると、古い半分を LLM で要約して `memories` に保存しメッセージを削除
|
||||
3. **コンテキスト合成**: `get_context_messages()` が `memories` の要約をシステムメッセージとして先頭に付加し、残存メッセージと結合して返す
|
||||
LLM に渡すコンテキストは `memory_kb/memory.py` の `get_context_messages()` が毎リクエスト組み立てる。
|
||||
以下の順に system メッセージを積み、最後に会話履歴を連結する。
|
||||
|
||||
| # | 層 | 中身 | 実装 | 量の制御 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 蓄積知識 | 過去の Web 検索で得た `knowledge_entries` の**要約全文**+出典 URL | `knowledge.get_knowledge_context()` | 最新 `KNOWLEDGE_CONTEXT_LIMIT`(既定 15)件 |
|
||||
| 2 | 共有ノート | ページの**タイトル一覧+本文先頭 40 文字**のみ | `notes.get_notes_context()` | 全ページ(本文は入れない) |
|
||||
| 3 | ローカル KB | `knowledge_base/**.md` から選ばれた**ファイル全文** | `local_knowledge.get_local_kb_context()` | 直近ユーザー発言との cos 類似度上位 `LOCAL_KB_TOP_K`(既定 3)件、かつ `LOCAL_KB_MIN_SCORE`(既定 0.75)以上 |
|
||||
| 4 | 長期メモリ | `memories` の要約を全件連結 | `memory.get_context_messages()` | 上限なし |
|
||||
| 5 | 短期メモリ | セッション内の `messages` 全件(要約済みで削除された分を除く) | 同上 | 上限なし(要約で間接的に制御) |
|
||||
|
||||
層 3 のプロンプトは「Web 検索結果より優先度が高い」「これで答えられるなら検索するな」と明示しており、層 1 より優先される。
|
||||
|
||||
#### 要約(長期メモリ化)
|
||||
|
||||
`maybe_summarize()` がアシスタント応答の保存直後に走る。
|
||||
|
||||
1. メッセージ数が `MEMORY_SUMMARIZE_THRESHOLD`(既定 20)以上になったら発火
|
||||
2. 古い側の半分(既定 10 件)を LLM で要約し `memories` に 1 行保存
|
||||
3. **要約した元メッセージは DB から物理削除**する(復元不可。エクスポートしても生ログは残らない)
|
||||
4. 保存した要約に `tagging.tag_memory()` でタグを付ける
|
||||
|
||||
#### ツール経由の遅延読み込み
|
||||
|
||||
常時注入すると重い情報は、システムプロンプトに目次だけ置いてツールで取りに行かせる。
|
||||
`llm.resolve_tool_calls()` が本番のストリーミング生成の**前に**別途 LLM を呼んでツール呼び出しを解決する(最大 2 ラウンド)。
|
||||
|
||||
- `read_note` / `write_note` … 共有ノート本文の読み書き(`db` を渡したときだけモデルに提示)
|
||||
- `web_search` … Tavily 検索。結果は `knowledge.summarize_results()` で構造化要約にされ、
|
||||
`knowledge_entries` と `knowledge_base/web_search/*.md` の両方に保存される。
|
||||
→ 次回以降の層 1・層 3 に自動的に効いてくる
|
||||
|
||||
#### 自動タグ付け・ワードプール
|
||||
|
||||
- `tagging.py` … 各メッセージに `asyncio.create_task()` でバックグラウンドにタグを 3 つ生成する。
|
||||
コンテキスト注入には使わず、Discord 雑談 Bot の話題選び(`find_related_tag()`)に使う
|
||||
- `word_memory.py` … Janome で名詞を抽出し `deque(maxlen=200)` にプロセス内保持する。
|
||||
**DB に永続化されず再起動で消える**(起動時に直近 200 件のメッセージから再シードされる)
|
||||
|
||||
#### 設計上の注意点
|
||||
|
||||
- トークン数ベースの制御は無く、すべて件数・文字数ベース。層 1(知識 15 件の要約全文)と層 3(Markdown 全文 ×3)は
|
||||
会話が短くてもコンテキストを大きく押し上げる
|
||||
- ツールを使わない会話でも `resolve_tool_calls()` が必ず 1 回フルコンテキストを投げるため、**LLM 呼び出しは最低 2 回**になる
|
||||
- 層 4(長期メモリ)に上限が無く、長いセッションでは要約が積み上がる(要約の再要約はしない)
|
||||
- `get_local_kb_context()` が毎リクエストで `sync_local_kb()`(ディレクトリスキャン)と埋め込み API 呼び出しを 1 回行う
|
||||
|
||||
### LLM 呼び出し(`core/llm.py`)
|
||||
|
||||
- `stream_chat()` / `complete()` が LiteLLM を薄くラップ
|
||||
- `_log_context` で呼び出しを囲み、成功・失敗どちらも `api_logs` テーブルに記録
|
||||
- `stream_chat()` / `complete()` / `embed()` が LiteLLM を薄くラップ
|
||||
- `resolve_tool_calls()` が `web_search` / `read_note` / `write_note` を解決し、結果を組み込んだメッセージ列を返す
|
||||
- `_log_context` で呼び出しを囲み、成功・失敗どちらも `api_logs` テーブルに記録(`embed()` は対象外)
|
||||
- `DEBUG=true` で `litellm._turn_on_debug()` が有効になり詳細ログが出力される
|
||||
|
||||
### 共有ノート(`core/notes.py` / `api/notes.py`)
|
||||
|
||||
ユーザーと AI が共同編集するノート。`note_pages` の 1 行が UI の 1 タブに対応する。
|
||||
|
||||
- `get_notes_context()` が**タイトル一覧のみ**をシステムメッセージとして生成し、`get_context_messages()` が会話コンテキストに差し込む(本文は含めない)
|
||||
- `get_notes_context()` が**タイトル一覧のみ**をシステムメッセージとして生成し、`get_context_messages()` が会話コンテキストに差し込む(本文は含めない)。セクションがあれば `## セクション名` の見出しでグループ化して提示する
|
||||
- 本文の読み書きはツール経由: `read_note`(本文取得)/ `write_note`(追記・置換・新規作成)
|
||||
- `llm.resolve_tool_calls()` が `web_search` と同じループでこれらを解決する。ノート系ツールは `db` を渡したときのみモデルに提示される
|
||||
- REST(`/api/notes`)はフロントのノートパネル用。GET 一覧 / POST 作成 / PATCH 更新 / DELETE 削除
|
||||
- REST(`/api/notes`)はフロントのノートパネル用。GET 一覧 / POST 作成 / PATCH 更新 / DELETE 削除 / `POST /api/notes/sections/rename`(セクション名の一括変更)
|
||||
- **セクションによるグルーピング**: `note_pages.section` に名前を持たせ、同じセクションのページは常に `position` 上で連続させる。`notes.set_section()` がページのセクションを変更するたびに全ページを再採番し、同じセクションの末尾に挿入し直す(隣接=同一グループという前提をタブ描画側・システムプロンプト側の両方が使っている)。`notes.rename_section()` は既に連続しているページ群のラベルだけを一括で書き換える
|
||||
- 既存 DB への追加カラムは Alembic 等の仕組みを使っておらず、`db/database.py` の `_add_missing_columns()` が起動時に `PRAGMA table_info` を見て `ALTER TABLE ... ADD COLUMN` を素朴に実行する(SQLite 専用の簡易マイグレーション)
|
||||
|
||||
### SSE ストリーミング(`api/chat.py`)
|
||||
|
||||
@@ -73,8 +143,9 @@ backend/
|
||||
|
||||
```
|
||||
data: {"type":"session_id","value":"<uuid>"} # 新規セッション時のみ
|
||||
data: {"type":"chunk","value":"テキスト断片"} # 0回以上
|
||||
data: {"type":"notes_updated"} # AIが共有ノートを更新した時のみ
|
||||
data: {"type":"sources","value":[{...}]} # Web検索した時のみ(出典の title/url)
|
||||
data: {"type":"chunk","value":"テキスト断片"} # 0回以上
|
||||
data: {"type":"done"} # 正常完了
|
||||
data: {"type":"error","value":"エラー内容"} # 例外発生時
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user