158 lines
13 KiB
Markdown
158 lines
13 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## プロジェクト概要
|
||
|
||
Python (FastAPI) バックエンド + Vue 3 (TypeScript) フロントエンドのチャットアプリ。
|
||
LLM は LiteLLM 経由で OpenAI / Anthropic / Ollama など任意のプロバイダーを統一 API で扱い、会話履歴とメモリは SQLite に永続化する。
|
||
|
||
## コマンド
|
||
|
||
### バックエンド
|
||
|
||
```powershell
|
||
Set-Location backend
|
||
|
||
# 依存インストール (SSL プロキシ環境では --native-tls が必要)
|
||
uv sync --native-tls
|
||
|
||
# 開発サーバー起動
|
||
uv run uvicorn app.main:app --reload
|
||
|
||
# テスト
|
||
pytest
|
||
pytest tests/test_chat.py # 単一ファイル
|
||
```
|
||
|
||
### フロントエンド
|
||
|
||
```powershell
|
||
Set-Location frontend
|
||
|
||
# 依存インストール
|
||
npm install
|
||
|
||
# 開発サーバー起動 (port 5173)
|
||
npm run dev
|
||
|
||
# ビルド
|
||
npm run build
|
||
|
||
# 型チェック
|
||
npm run type-check
|
||
```
|
||
|
||
### 環境設定
|
||
|
||
```powershell
|
||
Set-Location backend
|
||
Copy-Item .env.example .env
|
||
# .env を編集して API キーとモデル名を設定
|
||
```
|
||
|
||
## アーキテクチャ
|
||
|
||
詳細は `docs/architecture.md` を参照。
|
||
|
||
```
|
||
frontend (Vue3 + Pinia, :5173)
|
||
↕ REST + SSE (Vite proxy)
|
||
backend (FastAPI, :8000)
|
||
├── app/api/ チャット・メモリ・ノート・ログ・設定 のルーター
|
||
├── app/core/ LLM ラッパー・共有ノート・設定
|
||
│ ├── memory_kb/ コンテキスト管理(要約・蓄積知識・ローカルKB・タグ付け)
|
||
│ ├── search/ Tavily Web 検索
|
||
│ └── discord/ Discord Bot・定期通知
|
||
└── app/db/ SQLAlchemy モデル・DB 接続
|
||
↕
|
||
SQLite (playchat.db) + LiteLLM → 任意の LLM
|
||
```
|
||
|
||
### コンテキスト管理
|
||
|
||
LLM に渡すコンテキストは `app/core/memory_kb/memory.py` の `get_context_messages()` が毎リクエスト組み立てる。
|
||
以下の順に system メッセージを積み、最後に会話履歴を連結する(詳細は `docs/architecture.md`)。
|
||
|
||
1. **蓄積知識** (`memory_kb/knowledge.py`): 過去の Web 検索で得た `knowledge_entries` の**見出し一覧のみ**(`- 「クエリ」: 一行要約`)。本文と出典は含めず、必要なら `read_knowledge` ツールで読み出す。最新 `KNOWLEDGE_CONTEXT_LIMIT`(既定 15)件。セッションを横断する
|
||
2. **共有ノート** (`core/notes.py`): タイトル一覧+本文先頭 40 文字のプレビューのみ
|
||
2.5. **参照指定ノート** (`core/notes.py`): ユーザーがそのセッションに紐付けた(GUI のタブで 📌 を押した)ページの**本文全文**。`session_note_pages` テーブルで管理し、1 ページ `SESSION_NOTES_CONTENT_LIMIT`(既定 8000)文字で打ち切る。層 2 の一覧側には印が付き、同じ本文を `read_note` で読み直させない
|
||
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` に保存する。**要約した元メッセージは `archived_at` を立ててアーカイブされる**(物理削除はしない)。アーカイブ済みメッセージは `get_context_messages()` と要約の閾値判定の両方から除外されるため LLM には渡らないが、DB には残り、履歴表示・エクスポートからは参照できる。`memory_id` にどの要約に取り込まれたかを保持する。
|
||
|
||
**遅延読み込み**: 重い情報は目次だけ注入し、`llm.stream_tool_calls()`(結果だけ欲しい場合は同等のラッパー `resolve_tool_calls()`)が本番生成の前にツール(`web_search` / `read_note` / `write_note` / `read_knowledge`、最大 2 ラウンド)を解決する。前者は実行の開始・完了を逐次 yield し、`api/chat.py` がそれを SSE の `tool` イベントとして中継して GUI に進行状況を出す。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 回になる。
|
||
|
||
### 共有ノート
|
||
|
||
ユーザーと 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`)をサーバーの値で上書きしない
|
||
- **チャットへの紐付け**: `session_note_pages`(session_id × page_id)に登録したページは、本文全文がそのセッションのシステムプロンプトに毎回入る。`GET/PUT /chat/sessions/{id}/notes` で読み書きする。新規セッションではフロントがまだ session_id を持てないため、`POST /chat/stream` の `note_page_ids` でも同じ指定を送れ、コンテキスト組み立て前にサーバー側で反映される
|
||
- **取り込み**: `POST /api/notes/import`(multipart)が `.md` / `.txt` を 1 ファイル 1 ページ、`.zip` を中身ごと複数ページに変換する(ZIP 第 1 階層のフォルダ名がセクション)。タイトル衝突時は `(2)` を足して別ページを作り、対象外・サイズ超過・壊れた ZIP は黙って飛ばす。1 件も取り込めなければ 400。`python-multipart` が必要
|
||
- **ダウンロード**: `GET /api/notes/{page_id}/export` が 1 ページを Markdown、`GET /api/notes/export` が全ページを ZIP(1 ページ 1 Markdown、セクションはフォルダ、ファイル名の通し番号で `position` 順を保持)で返す。ファイル名はサーバーが決めるのでフロントは `download` 属性を付けず `Content-Disposition`(日本語タイトルのため RFC 5987 を併記)に任せる
|
||
- **セクションによるグルーピング**: `note_pages.section` にセクション名を持たせ、同じセクションのページは `position` 上で常に連続させる(`core/notes.set_section()` が割り当て時に並べ替える)。タブバーは `position` 順に描画し、隣接して同じ `section` が続く区間を1グループとして表示する。`get_notes_context()` はセクションごとに `## セクション名` の見出しを付けてタイトル一覧を出す。セクション名の一括変更は `POST /api/notes/sections/rename`(`core/notes.rename_section()`)
|
||
|
||
### SSE ストリーミング
|
||
|
||
`POST /chat/stream` が SSE を返す。イベント種別: `session_id`(新規セッション ID)/ `chunk`(テキスト断片)/ `tool`(ツール実行の開始・完了)/ `sources`(Web 検索の出典)/ `notes_updated`(AI が共有ノートを更新)/ `done`(完了)/ `error`(エラー)。
|
||
フロントは `src/api/client.ts` の `streamChat()` で fetch + ReadableStream により受信し、`messages.value[msgIdx]` を reactive proxy 経由で更新する。
|
||
|
||
### Markdown レンダリング
|
||
|
||
アシスタントメッセージは `marked` + `DOMPurify` でレンダリング。ストリーミング中の未閉じコードフェンスは自動補完してから描画する(`MarkdownContent.vue`)。
|
||
数式は `src/utils/math.ts` の marked 拡張が KaTeX で描画する(ブロック `$$...$$` / `\[...\]`、インライン `$...$` / `\(...\)`)。詳細は `docs/architecture.md` を参照。
|
||
|
||
### モデル切替
|
||
|
||
`.env` の `DEFAULT_MODEL` またはリクエストの `model` フィールドで指定。
|
||
ローカル Ollama は `ollama/llama3` 形式、`OLLAMA_API_BASE` でエンドポイントを設定。
|
||
`DEBUG=true` で LiteLLM の詳細ログが有効になる。
|
||
|
||
## ローカル開発環境メモ
|
||
|
||
- このマシンでは PostgreSQL がポート 8080 で起動している(本アプリの `DATABASE_URL` には使用していないが、ポート競合時は確認すること)
|
||
- このマシンには TLS 監視プロキシらしき設定があり、環境変数 `SSLKEYLOGFILE` が名前付きパイプ(`nllMonFltProxy`)を指している。これが原因で `litellm`(OpenSSL を直接ロード)や `pyright`(内部で Node.js サブプロセスを起動)が `OPENSSL_Uplink` エラーでクラッシュすることがある。発生した場合は当該コマンド実行時のみ `SSLKEYLOGFILE` を unset すると回避できる
|
||
```powershell
|
||
Remove-Item Env:\SSLKEYLOGFILE
|
||
uv run pyright
|
||
```
|
||
|
||
## 環境変数(backend/.env)
|
||
|
||
| 変数 | デフォルト | 説明 |
|
||
|---|---|---|
|
||
| `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 キー |
|
||
| `ANTHROPIC_API_KEY` | — | Anthropic API キー |
|
||
| `OLLAMA_API_BASE` | `http://localhost:11434` | Ollama エンドポイント |
|
||
| `NOTES_TOOL_CONTENT_LIMIT` | `4000` | `read_note` が 1 回に返す本文の最大文字数 |
|
||
| `SESSION_NOTES_CONTENT_LIMIT` | `8000` | セッションに紐付けたノート 1 ページあたり、システムプロンプトに載せる本文の最大文字数 |
|
||
| `NOTES_IMPORT_MAX_PAGES` | `100` | ノート取り込みで一度に作れるページ数の上限 |
|
||
| `NOTES_IMPORT_MAX_BYTES` | `2000000` | 取り込む 1 ファイル(ZIP 内は 1 エントリ)あたりの最大バイト数 |
|
||
| `KNOWLEDGE_TOOL_CONTENT_LIMIT` | `4000` | `read_knowledge` が 1 回に返す本文の最大文字数 |
|
||
| `SITE_PASSWORD` | — | サイト全体を保護する共有パスワード(未設定なら無認証) |
|
||
| `SESSION_SECRET_KEY` | 起動毎に自動生成 | ログインセッション Cookie の署名鍵 |
|