Files
playchat/CLAUDE.md
T
colcaroliandClaude Opus 5 366320c43a Show tool and web search activity in the chat UI
Tool calls were invisible until the answer arrived: the only trace was the
sources list, and only for web searches. Surface them as they happen instead.

Add llm.stream_tool_calls(), an incremental version of resolve_tool_calls()
that yields a start and a completion event per tool call and ends with a
"resolved" event carrying the previous return value. resolve_tool_calls() is
kept as a thin wrapper that drops the progress events.

api/chat.py relays those events as a new SSE "tool" event, and wraps the
search-summary/knowledge-save step in one of the same shape, since it is a
noticeable pause of its own.

On the front end, ToolActivityList.vue renders one row per tool above the
assistant bubble (label, target, spinner while running, short result when
done). The store merges events by id and marks anything still running as
interrupted when the stream ends early. Display only - not persisted, so the
rows disappear when the session is reloaded, same as sources.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:11:00 +09:00

10 KiB
Raw Blame History

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 に永続化する。

コマンド

バックエンド

Set-Location backend

# 依存インストール (SSL プロキシ環境では --native-tls が必要)
uv sync --native-tls

# 開発サーバー起動
uv run uvicorn app.main:app --reload

# テスト
pytest
pytest tests/test_chat.py        # 単一ファイル

フロントエンド

Set-Location frontend

# 依存インストール
npm install

# 開発サーバー起動 (port 5173)
npm run dev

# ビルド
npm run build

# 型チェック
npm run type-check

環境設定

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 文字のプレビューのみ
  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)をサーバーの値で上書きしない
  • セクションによるグルーピング: 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)。

モデル切替

.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 すると回避できる
    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 回に返す本文の最大文字数
KNOWLEDGE_TOOL_CONTENT_LIMIT 4000 read_knowledge が 1 回に返す本文の最大文字数
SITE_PASSWORD — サイト全体を保護する共有パスワード(未設定なら無認証)
SESSION_SECRET_KEY 起動毎に自動生成 ログインセッション Cookie の署名鍵