# アーキテクチャ概要 ## 全体構成 ``` フロントエンド (Vue 3 + TypeScript + Vite, :5173) ↕ REST + SSE (Vite dev proxy 経由) バックエンド (FastAPI + Python, :8000) ↕ SQLAlchemy SQLite (playchat.db) ↕ LiteLLM OpenAI / Anthropic / Ollama など任意の LLM ``` --- ## バックエンド ### ディレクトリ構成 ``` backend/ ├── app/ │ ├── main.py # FastAPI インスタンス、CORS、ルーター登録、DB初期化 │ ├── api/ │ │ ├── chat.py # チャット・セッション関連エンドポイント │ │ ├── memory.py # 長期メモリ参照・削除 │ │ └── logs.py # API ログ参照・削除 │ ├── core/ │ │ ├── config.py # .env 読み込み・定数定義 │ │ ├── llm.py # LiteLLM ラッパー(ストリーミング・ログ記録) │ │ └── memory.py # メモリ読み書きロジック(要約・コンテキスト生成) │ └── db/ │ ├── database.py # SQLite 接続・セッションファクトリ・テーブル自動作成 │ └── models.py # SQLAlchemy モデル定義 └── pyproject.toml ``` ### DB テーブル | テーブル | 主なカラム | 用途 | |---|---|---| | `sessions` | id (UUID), title, model, created_at, updated_at | チャットセッション | | `messages` | id, session_id, role, content, created_at | 会話メッセージ(短期メモリ) | | `memories` | id, session_id, summary, embedding_json, created_at | 要約による長期メモリ | | `note_pages` | id, title, content, position, created_at, updated_at | ユーザーと AI が共同編集する共有ノートのページ | | `api_logs` | id, session_id, model, request_json, response_text, error, latency_ms, created_at | LLM API 呼び出しログ | ### メモリ機構(`core/memory.py`) 1. **短期メモリ**: セッション内の全 `messages` をコンテキストとして送信 2. **長期メモリ**: メッセージ数が `MEMORY_SUMMARIZE_THRESHOLD`(デフォルト 20)を超えると、古い半分を LLM で要約して `memories` に保存しメッセージを削除 3. **コンテキスト合成**: `get_context_messages()` が `memories` の要約をシステムメッセージとして先頭に付加し、残存メッセージと結合して返す ### LLM 呼び出し(`core/llm.py`) - `stream_chat()` / `complete()` が LiteLLM を薄くラップ - `_log_context` で呼び出しを囲み、成功・失敗どちらも `api_logs` テーブルに記録 - `DEBUG=true` で `litellm._turn_on_debug()` が有効になり詳細ログが出力される ### 共有ノート(`core/notes.py` / `api/notes.py`) ユーザーと AI が共同編集するノート。`note_pages` の 1 行が UI の 1 タブに対応する。 - `get_notes_context()` が**タイトル一覧のみ**をシステムメッセージとして生成し、`get_context_messages()` が会話コンテキストに差し込む(本文は含めない) - 本文の読み書きはツール経由: `read_note`(本文取得)/ `write_note`(追記・置換・新規作成) - `llm.resolve_tool_calls()` が `web_search` と同じループでこれらを解決する。ノート系ツールは `db` を渡したときのみモデルに提示される - REST(`/api/notes`)はフロントのノートパネル用。GET 一覧 / POST 作成 / PATCH 更新 / DELETE 削除 ### SSE ストリーミング(`api/chat.py`) `POST /chat/stream` は `StreamingResponse` で以下のイベントを順番に送出する: ``` data: {"type":"session_id","value":""} # 新規セッション時のみ data: {"type":"chunk","value":"テキスト断片"} # 0回以上 data: {"type":"notes_updated"} # AIが共有ノートを更新した時のみ data: {"type":"done"} # 正常完了 data: {"type":"error","value":"エラー内容"} # 例外発生時 ``` バッファリング防止のため `Cache-Control: no-cache` と `X-Accel-Buffering: no` を付与する。 --- ## フロントエンド ### ディレクトリ構成 ``` frontend/src/ ├── api/ │ └── client.ts # fetch ラッパー・SSE クライアント・型定義 ├── stores/ │ └── chat.ts # Pinia ストア(全アプリ状態) ├── components/ │ ├── SessionList.vue # セッション一覧サイドバー │ ├── ChatWindow.vue # メッセージ表示エリア │ ├── MessageInput.vue # 入力欄・送信 │ ├── ModelSelector.vue # LLM モデル切替セレクト │ ├── MarkdownContent.vue # marked + DOMPurify によるレンダリング │ ├── LogViewer.vue # API ログ閲覧パネル │ └── ToastList.vue # エラートースト通知 └── App.vue # ルートレイアウト ``` ### 状態管理(`stores/chat.ts`) | 状態 | 型 | 説明 | |---|---|---| | `sessions` | `SessionInfo[]` | セッション一覧 | | `currentSessionId` | `string \| null` | 選択中セッション | | `messages` | `Message[]` | 表示中メッセージ | | `isStreaming` | `boolean` | SSE 受信中フラグ | | `selectedModel` | `string` | 選択中 LLM モデル | | `toasts` | `Toast[]` | エラー通知キュー | ### SSE 受信フロー 1. `sendMessage()` がユーザーメッセージと空のアシスタントメッセージを `messages` に追加 2. `streamChat()` が fetch + ReadableStream で SSE を受信 3. `chunk` イベント受信のたびに `messages.value[msgIdx].content` を更新(Vue reactive proxy 経由) 4. `done` で `pending` フラグを解除 5. `error` / ネットワークエラーは `error` フラグを立て赤バブル表示 + トースト通知 ### Markdown レンダリング(`MarkdownContent.vue`) - アシスタントメッセージのみ `marked` で HTML に変換後 `DOMPurify` でサニタイズ - ストリーミング中(`pending=true`)は未閉じのコードフェンス(` ``` `)を自動補完してから描画 ### Vite プロキシ設定 `/chat`、`/memory`、`/logs`、`/health` を `http://localhost:8000` に転送。 `/chat` は SSE バッファリング無効化のため `configure` で `x-accel-buffering: no` を付与。