From 1c21fc3406418334b28b33c9ee22454dc4d15068 Mon Sep 17 00:00:00 2001 From: zukki259 Date: Thu, 20 Aug 2026 00:28:36 +0900 Subject: [PATCH] Add section grouping to shared note tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 40 ++++++-- backend/app/api/notes.py | 24 ++++- backend/app/core/notes.py | 52 +++++++++- backend/app/db/database.py | 13 ++- backend/app/db/models.py | 3 +- docs/api.md | 32 ++++++- docs/architecture.md | 109 +++++++++++++++++---- frontend/src/api/client.ts | 17 +++- frontend/src/components/NotePanel.vue | 132 ++++++++++++++++++++------ frontend/src/stores/notes.ts | 34 ++++++- 10 files changed, 388 insertions(+), 68 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 94a45f8..25d555f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,17 +59,34 @@ Copy-Item .env.example .env frontend (Vue3 + Pinia, :5173) ↕ REST + SSE (Vite proxy) backend (FastAPI, :8000) - ├── app/api/ チャット・メモリ・ノヌト・ログ のルヌタヌ - ├── app/core/ LLM ラッパヌ・メモリロゞック・共有ノヌト・蚭定 - └── app/db/ SQLAlchemy モデル・DB 接続 + ├── app/api/ チャット・メモリ・ノヌト・ログ・蚭定 のルヌタヌ + ├── app/core/ LLM ラッパヌ・共有ノヌト・蚭定 + │ ├── memory_kb/ コンテキスト管理芁玄・蓄積知識・ロヌカルKB・タグ付け + │ ├── search/ Tavily Web 怜玢 + │ └── discord/ Discord Bot・定期通知 + └── app/db/ SQLAlchemy モデル・DB 接続 ↕ SQLite (playchat.db) + LiteLLM → 任意の LLM ``` -### メモリ機構 +### コンテキスト管理 -- **短期**: セッション内の党メッセヌゞをコンテキストに含める -- **長期**: メッセヌゞ数が `MEMORY_SUMMARIZE_THRESHOLD`デフォルト 20を超えるず叀い半分を LLM で芁玄 → `memories` テヌブルに保存 → 次回以降のシステムプロンプトに泚入 +LLM に枡すコンテキストは `app/core/memory_kb/memory.py` の `get_context_messages()` が毎リク゚スト組み立おる。 +以䞋の順に system メッセヌゞを積み、最埌に䌚話履歎を連結する詳现は `docs/architecture.md`。 + +1. **蓄積知識** (`memory_kb/knowledge.py`): 過去の Web 怜玢で埗た `knowledge_entries` の芁玄党文出兞。最新 `KNOWLEDGE_CONTEXT_LIMIT`既定 15件。セッションを暪断する +2. **共有ノヌト** (`core/notes.py`): タむトル䞀芧本文先頭 40 文字のプレビュヌのみ +3. **ロヌカル KB** (`memory_kb/local_knowledge.py`): `backend/knowledge_base/**.md` のうち、盎近ナヌザヌ発蚀ず埋め蟌みコサむン類䌌床が高い䞊䜍 `LOCAL_KB_TOP_K`既定 3件か぀ `LOCAL_KB_MIN_SCORE`既定 0.75以䞊を**å…šæ–‡**泚入。プロンプト䞊は Web 怜玢結果より優先する扱い +4. **長期メモリ**: `memories` の芁玄を党件連結。䞊限なし +5. **短期メモリ**: セッション内の `messages` 党件芁玄で削陀された分を陀く + +**芁玄**: `maybe_summarize()` がアシスタント応答の保存盎埌に走り、メッセヌゞ数が `MEMORY_SUMMARIZE_THRESHOLD`既定 20以䞊になるず叀い半分を LLM で芁玄しお `memories` に保存する。**芁玄した元メッセヌゞは物理削陀される**埩元䞍可。 + +**遅延読み蟌み**: 重い情報は目次だけ泚入し、`llm.resolve_tool_calls()` が本番生成の前にツヌル`web_search` / `read_note` / `write_note`、最倧 2 ラりンドを解決する。Web 怜玢の結果は構造化芁玄にされ `knowledge_entries` ず `knowledge_base/web_search/*.md` の䞡方に保存されるため、次回以降は局 1・局 3 ずしお自動的に効いおくる。 + +**付随機胜**: `memory_kb/tagging.py` が各メッセヌゞ・芁玄にバックグラりンドでタグを 3 ぀付けるコンテキストには泚入せず、Discord 雑談 Bot の話題遞びに䜿う。`memory_kb/word_memory.py` は Janome で抜出した名詞を `deque(maxlen=200)` にプロセス内保持するDB 非氞続。起動時に盎近 200 件から再シヌド。 + +**倉曎時の泚意**: トヌクン数ベヌスの制埡は䞀切なく、すべお件数・文字数ベヌス。たたツヌルを䜿わない䌚話でも `resolve_tool_calls()` がフルコンテキストを 1 回投げるため、1 応答あたり LLM 呌び出しは最䜎 2 回になる。 ### 共有ノヌト @@ -80,6 +97,7 @@ SQLite (playchat.db) + LiteLLM → 任意の LLM - **ツヌル実行の流れ**: `llm.resolve_tool_calls()` が本番の応答生成前にツヌル呌び出しを解決する。`web_search` ず共通の仕組みで、ノヌト系ツヌルは `db` が枡されたずきだけモデルに提瀺される - **UI ぞの反映**: AI がノヌトを曞き換えた堎合、SSE で `notes_updated` を送っおフロント偎のノヌトパネルを再読み蟌みさせる - **線集の衝突回避**: フロントは未保存の線集があるペヌゞ`dirtyIds`をサヌバヌの倀で䞊曞きしない +- **セクションによるグルヌピング**: `note_pages.section` にセクション名を持たせ、同じセクションのペヌゞは `position` 䞊で垞に連続させる`core/notes.set_section()` が割り圓お時に䞊べ替える。タブバヌは `position` 順に描画し、隣接しお同じ `section` が続く区間を1グルヌプずしお衚瀺する。`get_notes_context()` はセクションごずに `## セクション名` の芋出しを付けおタむトル䞀芧を出す。セクション名の䞀括倉曎は `POST /api/notes/sections/rename``core/notes.rename_section()` ### SSE ストリヌミング @@ -109,9 +127,17 @@ SQLite (playchat.db) + LiteLLM → 任意の LLM | 倉数 | デフォルト | 説明 | |---|---|---| -| `DEFAULT_MODEL` | `gpt-5.4` | デフォルト LLM モデル | +| `DEFAULT_MODEL` | `gpt-4o` | デフォルト LLM モデル`.env.example` では `gpt-5.4` | | `DATABASE_URL` | `sqlite:///./playchat.db` | DB 接続先 | | `MEMORY_SUMMARIZE_THRESHOLD` | `20` | 長期メモリ化するメッセヌゞ数の閟倀 | +| `KNOWLEDGE_CONTEXT_LIMIT` | `15` | 蓄積知識をシステムプロンプトに泚入する最倧件数 | +| `TAVILY_API_KEY` | — | Tavily Web 怜玢の API キヌ未蚭定なら `web_search` は空を返す | +| `SEARCH_SUMMARY_MODEL` | — | Web 怜玢結果の芁玄に䜿うモデル未蚭定なら `DEFAULT_MODEL` | +| `TAGGING_MODEL` | — | メッセヌゞの自動タグ付けに䜿うモデル未蚭定なら `DEFAULT_MODEL` | +| `LOCAL_KB_DIR` | `knowledge_base` | ロヌカル Markdown ナレッゞベヌスのディレクトリ | +| `LOCAL_KB_EMBEDDING_MODEL` | `text-embedding-3-small` | ロヌカル KB の埋め蟌みモデルGUI 蚭定画面から実行時倉曎可 | +| `LOCAL_KB_TOP_K` | `3` | ロヌカル KB から泚入する最倧ファむル数 | +| `LOCAL_KB_MIN_SCORE` | `0.75` | ロヌカル KB を泚入するコサむン類䌌床の䞋限 | | `CORS_ORIGINS` | `http://localhost:5173` | 蚱可する CORS オリゞン | | `DEBUG` | `false` | LiteLLM デバッグログの有効化 | | `OPENAI_API_KEY` | — | OpenAI API キヌ | diff --git a/backend/app/api/notes.py b/backend/app/api/notes.py index 998cf00..d5c2fac 100644 --- a/backend/app/api/notes.py +++ b/backend/app/api/notes.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.orm import Session as DBSession -from app.core.notes import next_position +from app.core.notes import next_position, rename_section, set_section from app.db.database import get_db from app.db.models import NotePage @@ -16,6 +16,7 @@ class NotePageResponse(BaseModel): title: str content: str position: int + section: str | None updated_at: datetime class Config: @@ -25,11 +26,18 @@ class NotePageResponse(BaseModel): class NotePageCreate(BaseModel): title: str = "新しいペヌゞ" content: str = "" + section: str | None = None class NotePageUpdate(BaseModel): title: str | None = None content: str | None = None + section: str | None = None + + +class SectionRenameRequest(BaseModel): + old_name: str + new_name: str | None = None @router.get("", response_model=list[NotePageResponse]) @@ -44,9 +52,18 @@ def create_page(req: NotePageCreate, db: DBSession = Depends(get_db)): db.add(page) db.commit() db.refresh(page) + if req.section is not None and req.section.strip(): + page = set_section(db, page.id, req.section) return page +@router.post("/sections/rename") +def rename_section_endpoint(req: SectionRenameRequest, db: DBSession = Depends(get_db)): + """old_name セクションに属する党ペヌゞを new_name セクションぞ䞀括で付け替える。new_name が空ならグルヌプ解陀。""" + count = rename_section(db, req.old_name, req.new_name) + return {"renamed": count} + + @router.patch("/{page_id}", response_model=NotePageResponse) def update_page(page_id: int, req: NotePageUpdate, db: DBSession = Depends(get_db)): page = db.get(NotePage, page_id) @@ -61,6 +78,11 @@ def update_page(page_id: int, req: NotePageUpdate, db: DBSession = Depends(get_d page.content = req.content db.commit() db.refresh(page) + + # section はペヌゞの䞊び順にも圱響するため、倉曎があれば専甚ロゞックで反映する + if "section" in req.model_fields_set: + page = set_section(db, page_id, req.section) + return page diff --git a/backend/app/core/notes.py b/backend/app/core/notes.py index 4db7e2c..ee7dd69 100644 --- a/backend/app/core/notes.py +++ b/backend/app/core/notes.py @@ -75,17 +75,65 @@ def next_position(db: DBSession) -> int: return (last.position + 1) if last else 0 +def set_section(db: DBSession, page_id: int, section: str | None) -> NotePage | None: + """ペヌゞの所属セクションを倉曎し、同じセクションのペヌゞ矀の盎埌に連続配眮し盎す。 + + タブバヌはposition順に描画され、隣接しお同じsectionが続く区間を1぀のグルヌプずしお + たずめお衚瀺するapp.api.notes / フロントのグルヌピング衚瀺を参照。そのため + セクションを倉曎したペヌゞは、垞に同じセクションの最埌のペヌゞの盎埌に移動する。 + """ + page = db.get(NotePage, page_id) + if not page: + return None + + normalized = (section or "").strip() or None + ordered = [p for p in db.query(NotePage).order_by(NotePage.position, NotePage.id).all() if p.id != page_id] + + if normalized is None: + insert_at = len(ordered) + else: + matching = [i for i, p in enumerate(ordered) if p.section == normalized] + insert_at = (matching[-1] + 1) if matching else len(ordered) + + ordered.insert(insert_at, page) + page.section = normalized + for i, p in enumerate(ordered): + p.position = i + db.commit() + db.refresh(page) + return page + + +def rename_section(db: DBSession, old_name: str, new_name: str | None) -> int: + """あるセクションに属する党ペヌゞのセクション名を䞀括で倉曎する。new_name が空ならグルヌプ解陀になる。 + + 戻り倀: 倉曎したペヌゞ数。 + """ + normalized_new = (new_name or "").strip() or None + pages = db.query(NotePage).filter(NotePage.section == old_name).order_by(NotePage.position, NotePage.id).all() + for page in pages: + page.section = normalized_new + db.commit() + return len(pages) + + def get_notes_context(db: DBSession) -> str | None: - """共有ノヌトのタむトル䞀芧をシステムプロンプト甚のテキストずしお返す。""" + """共有ノヌトのタむトル䞀芧をシステムプロンプト甚のテキストずしお返す。セクションごずに芋出しを付ける。""" pages = db.query(NotePage).order_by(NotePage.position, NotePage.id).all() lines = [ "【共有ノヌト】", - "ナヌザヌずあなたが共同で線集できるノヌトです。以䞋のペヌゞがありたす。", + "ナヌザヌずあなたが共同で線集できるノヌトです。以䞋のペヌゞがありたす" + "セクションはペヌゞをグルヌプ化する芋出しで、あれば䜵蚘したす。", "", ] if pages: + current_section = "__unset__" # どのペヌゞのsectionずも䞀臎しない番兵 for page in pages: + if page.section != current_section: + current_section = page.section + if current_section: + lines.append(f"## {current_section}") preview = " ".join(page.content.split())[:40] suffix = f" — {preview}
" if preview else " — (空)" lines.append(f"- {page.title}{suffix}") diff --git a/backend/app/db/database.py b/backend/app/db/database.py index b545a38..73e685d 100644 --- a/backend/app/db/database.py +++ b/backend/app/db/database.py @@ -1,4 +1,4 @@ -from sqlalchemy import create_engine +from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker, DeclarativeBase from app.core.config import DATABASE_URL @@ -18,6 +18,17 @@ def get_db(): db.close() +def _add_missing_columns() -> None: + """マむグレヌションフレヌムワヌクを䜿っおいないため、既存DBに埌から远加されたカラムを + ここで簡易的に補う。create_all() は既存テヌブルぞのカラム远加は行わないので必芁。""" + with engine.connect() as conn: + existing = {row[1] for row in conn.execute(text("PRAGMA table_info(note_pages)"))} + if existing and "section" not in existing: + conn.execute(text("ALTER TABLE note_pages ADD COLUMN section VARCHAR")) + conn.commit() + + def init_db(): from app.db import models # noqa: F401 Base.metadata.create_all(bind=engine) + _add_missing_columns() diff --git a/backend/app/db/models.py b/backend/app/db/models.py index 390fcb6..9693385 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -115,7 +115,8 @@ class NotePage(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) title: Mapped[str] = mapped_column(String) content: Mapped[str] = mapped_column(Text, default="") - position: Mapped[int] = mapped_column(Integer, default=0) # タブの䞊び順 + position: Mapped[int] = mapped_column(Integer, default=0) # タブの䞊び順セクションでたずめる際、同じセクションのペヌゞは連続させる + section: Mapped[str | None] = mapped_column(String, nullable=True) # タブをグルヌプ化するセクション名。未蚭定なら未分類 created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc)) updated_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) diff --git a/docs/api.md b/docs/api.md index 0bd7ca2..75b77cd 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 } ``` --- diff --git a/docs/architecture.md b/docs/architecture.md index 8749c32..f49e890 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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":""} # 新芏セッション時のみ -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":"゚ラヌ内容"} # 䟋倖発生時 ``` diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 670d253..923cec4 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -263,6 +263,7 @@ export interface NotePage { title: string content: string position: number + section: string | null updated_at: string } @@ -271,18 +272,18 @@ export async function fetchNotePages(): Promise { return parseJson(res, 'ノヌト取埗') } -export async function createNotePage(title: string): Promise { +export async function createNotePage(title: string, section?: string | null): Promise { const res = await fetch(`${BASE}/api/notes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title }), + body: JSON.stringify({ title, section: section ?? null }), }) return parseJson(res, 'ペヌゞ䜜成') } export async function updateNotePage( pageId: number, - patch: { title?: string; content?: string }, + patch: { title?: string; content?: string; section?: string | null }, ): Promise { const res = await fetch(`${BASE}/api/notes/${pageId}`, { method: 'PATCH', @@ -295,3 +296,13 @@ export async function updateNotePage( export async function deleteNotePage(pageId: number): Promise { await fetch(`${BASE}/api/notes/${pageId}`, { method: 'DELETE' }) } + +/** section = old のペヌゞ党䜓を section = new ぞ䞀括で付け替える。new が空/nullならグルヌプ解陀。 */ +export async function renameNoteSection(oldName: string, newName: string | null): Promise<{ renamed: number }> { + const res = await fetch(`${BASE}/api/notes/sections/rename`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ old_name: oldName, new_name: newName }), + }) + return parseJson<{ renamed: number }>(res, 'セクション名倉曎') +} diff --git a/frontend/src/components/NotePanel.vue b/frontend/src/components/NotePanel.vue index c07efc3..efb5642 100644 --- a/frontend/src/components/NotePanel.vue +++ b/frontend/src/components/NotePanel.vue @@ -1,15 +1,36 @@