Files
playchat/docs/api.md
colcaroliandClaude Opus 5 97568ba90e Re-embed the knowledge base when the embedding model changes
Vectors from two different embedding models are not comparable — Titan v2 has
1024 dimensions where nomic-embed-text has 768 — and _cosine_similarity zips
them, so a mismatch produced quietly meaningless scores rather than an error.
Nothing forced a re-embed either: sync_local_kb only looked at the content
hash, so switching models left every cached vector in place.

local_knowledge_docs now records the model each vector came from, and a row is
re-embedded when its content hash OR its model differs. Saving a new
LOCAL_KB_EMBEDDING_MODEL from the settings panel starts a background pass over
every file; the knowledge base panel also has a button for it. While that pass
runs, the per-request sync steps aside so no file is embedded twice.

The panel now shows what the embedding model is doing: model name, vector
size, how many files are embedded with the current model, how many are still
on an older one, a progress bar while a pass runs, and the last error. A
"接続テスト" button embeds one short string so a broken or unreachable model
can be told apart from one that is merely slow to warm up.

Also read TAVILY_API_KEY from os.environ at call time. It was imported as a
config constant, which is evaluated before load_into_env() runs, so a key
saved from the settings panel was stored but never used — not even after a
restart.

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

391 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API リファレンス
ベースURL: `http://localhost:8000`
Swagger UI: `http://localhost:8000/docs`
---
## チャット
### POST /chat/stream
SSE でストリーミング応答を返す。
**リクエストボディ**
```json
{
"message": "こんにちは",
"session_id": "uuid(省略時は新規セッション作成)",
"model": "gpt-5.4(省略時は DEFAULT_MODEL)",
"note_page_ids": [1, 2]
}
```
`note_page_ids` は「このセッションで本文をシステムプロンプトに載せる共有ノートのページ ID」。
渡すとコンテキスト組み立ての前にサーバー側の指定を丸ごと置き換える(`PUT /chat/sessions/{id}/notes` と同じ内容)。
新規セッションではフロントがまだ session_id を持てないため、初回はここでしか指定できない。省略すると現在の指定を変更しない。
**SSE イベント**
| type | value | タイミング |
|---|---|---|
| `session_id` | セッション UUID | 新規セッション作成直後 |
| `chunk` | テキスト断片 | LLM トークン生成ごと |
| `tool` | `{id, name, label, detail, status, result}` | ツール実行の開始時 (`status: running`) と完了時 (`status: done`)。同じ `id` で 2 回届くのでフロントはマージして表示する |
| `sources` | `[{title, url}]` | Web 検索した時のみ(出典一覧) |
| `notes_updated` | — | AI が共有ノートを書き換えた直後 |
| `done` | — | 正常完了 |
| `error` | エラーメッセージ | 例外発生時 |
---
### GET /chat/sessions
セッション一覧を `updated_at` 降順で返す。
**レスポンス**
```json
[
{
"id": "uuid",
"title": "セッションタイトル",
"model": "gpt-5.4",
"updated_at": "2026-06-01T12:00:00"
}
]
```
---
### GET /chat/sessions/{session_id}/messages
セッションのメッセージ一覧を `created_at` 昇順で返す。要約に取り込まれた(アーカイブ済みの)
メッセージも含めて返し、`archived_at` で区別する。アーカイブ済みのものは LLM に渡す
コンテキストには入らないが、履歴としては残っている。
**レスポンス**
```json
[
{ "id": 1, "role": "user", "content": "...", "created_at": "...", "archived_at": "2026-08-20T04:00:00+00:00", "tags": [] },
{ "id": 2, "role": "assistant", "content": "...", "created_at": "...", "archived_at": null, "tags": [] }
]
```
---
### GET /chat/sessions/{session_id}/notes
このセッションで本文をシステムプロンプトに載せるよう指定された共有ノートのページ ID を返す。
順序はノートのタブ順(`position`)。セッションが無ければ `404`。
**レスポンス**
```json
{ "page_ids": [1, 2] }
```
---
### PUT /chat/sessions/{session_id}/notes
指定を丸ごと置き換える。存在しないページ ID は黙って捨てられ、実際に紐付いた ID が返る。
空配列を渡すと全解除。セッションが無ければ `404`。
**リクエスト**
```json
{ "page_ids": [1, 2] }
```
---
### DELETE /chat/sessions/{session_id}
セッションと紐づくメッセージ・メモリをすべて削除。`204 No Content` を返す。
---
## メモリ
### GET /memory/{session_id}
セッションの長期メモリ(要約)一覧を `created_at` 昇順で返す。
**レスポンス**
```json
[
{
"id": 1,
"session_id": "uuid",
"summary": "要約テキスト",
"created_at": "2026-06-01T12:00:00"
}
]
```
---
### DELETE /memory/{memory_id}
特定のメモリエントリを削除。`204 No Content` を返す。
---
## 共有ノート
ユーザーと AI が共同編集するノート。ページのタイトル一覧は毎回システムプロンプトに注入され、
本文は AI が `read_note` / `write_note` ツールで読み書きする。
`section` を設定すると、同じセクション名のページはタブバー上で常に連続してグループ表示される。
### GET /api/notes
全ページを `position` 昇順で返す。同じ `section` のページは常に連続する(隣接=同一グループとして扱われる)。
**レスポンス**
```json
[
{
"id": 1,
"title": "買い物リスト",
"content": "- 牛乳
- 卵",
"position": 0,
"section": "生活",
"updated_at": "2026-08-19T12:00:00"
}
]
```
---
### POST /api/notes
ページを新規作成し、`201 Created` で作成したページを返す。
`section` を指定すると、作成直後にそのセクションの末尾へ配置される(他ページの `position` も再採番される)。
**リクエスト**
```json
{ "title": "新しいページ", "content": "", "section": null }
```
---
### PATCH /api/notes/{page_id}
ページのタイトル・本文・セクションを更新する。指定したフィールドだけが変更される。
タイトルを空文字にしようとした場合は `400`、ページが存在しない場合は `404` を返す。
`section` はリクエストボディに**キーとして含まれているかどうか**で判定する(`null` を明示的に送るとグループ解除)。
セクションを変更すると、そのページは新しいセクションの末尾(該当ページが無ければ末尾)へ移動し、
全ページの `position` が振り直される。
**リクエスト**
```json
{ "content": "更新後の本文", "section": "生活" }
```
---
### POST /api/notes/sections/rename
`old_name` セクションに属する全ページを `new_name` セクションへ一括で付け替える。
`new_name` を空文字/未指定にすると、そのセクションの全ページのグループを解除する。
ページの並び順(`position`)は変更しない。
**リクエスト**
```json
{ "old_name": "生活", "new_name": "生活雑貨" }
```
**レスポンス**
```json
{ "renamed": 2 }
```
---
### POST /api/notes/reorder
タブのドラッグ&ドロップによる並べ替えを反映する。`order` には**全ページID**をドラッグ後の
新しい順序で渡す(サーバー側では計算しない。フロントが `store.pages` から算出する)。
`page_id` のページには先に `section` を設定してから、`order` の順に `position` を振り直す。
別セクションのタブの前後にドロップした場合は、そのセクションへの移動も兼ねる。
`order` が現在の全ページIDの集合と一致しない場合は `400` を返す。
**リクエスト**
```json
{ "page_id": 3, "section": "生活", "order": [1, 3, 2, 4] }
```
**レスポンス**
```json
{ "reordered": 4 }
```
---
### DELETE /api/notes/{page_id}
ページを削除。`204 No Content` を返す。
---
### POST /api/notes/import
`multipart/form-data` の `files` でアップロードされたファイルをページとして取り込み、
`201 Created` で作成したページの配列を返す。
| 拡張子 | 取り込み方 |
|---|---|
| `.md` / `.markdown` / `.txt` / `.text` | 1 ファイル 1 ページ。タイトルはファイル名から拡張子と `01_` 形式の通し番号を落としたもの |
| `.zip` | 中のテキストファイルを 1 つずつページにする。第 1 階層のフォルダ名がセクションになる(`GET /api/notes/export` の ZIP をそのまま戻せる) |
- 既存ページとタイトルが衝突したら `タイトル (2)` のように連番を足して**別ページとして作る**(上書きはしない)
- 文字コードは UTF-8 (BOM 可) → CP932 の順に試す
- 対象外の拡張子・`NOTES_IMPORT_MAX_BYTES`(既定 2,000,000)超のファイル・`NOTES_IMPORT_MAX_PAGES`(既定 100)を超えた分・壊れた ZIP は**黙って飛ばす**
- 1 ページも取り込めなかった場合のみ `400`
---
### GET /api/notes/export
全ページを ZIP でダウンロードさせる(`Content-Disposition: attachment`)。
1 ページ 1 ファイルの Markdown で、本文はそのまま。エントリ名は `<セクション>/<通し番号>_<タイトル>.md`
(セクション無しのページは ZIP 直下)。通し番号は `position` 順なのでタブの並びが残る。
ファイル名は `notes_YYYYMMDD.zip`。
---
### GET /api/notes/{page_id}/export
1 ページを Markdown ファイルとしてダウンロードさせる。本文はそのままで、ファイル名がタイトルになる。
ページが存在しない場合は `404`。
日本語タイトルを扱うため `Content-Disposition` には RFC 5987 形式を併記する:
```
attachment; filename="______.md"; filename*=UTF-8''%E8%B2%B7%E3%81%84%E7%89%A9%E3%83%AA%E3%82%B9%E3%83%88.md
```
ファイル名に使えない文字(`\ / : * ? " < > |` と制御文字)は `_` に置き換え、80 文字で切る。
---
## ローカルナレッジベース(閲覧)
`backend/knowledge_base/**.md` を GUI から読むための読み取り専用 API。編集・削除はできない。
### GET /api/kb/files
ナレッジベース配下の Markdown を、パス順の一覧で返す(本文は含めない)。
```json
[
{
"path": "web_search/02b627ac75bb2fd4.md",
"name": "02b627ac75bb2fd4.md",
"directory": "web_search",
"title": "SOUL.md use-soul-with-hermes",
"preview": "SOUL.mdは、Hermes Agent インスタンスの主要な…",
"size": 6150,
"updated_at": "2026-06-21T05:03:52.684324Z",
"indexed": true
}
]
```
`directory` はナレッジベース直下なら空文字。`indexed` は `local_knowledge_docs` に埋め込み済み
(=話題に応じた自動注入の対象になっている)かどうか。未埋め込みのファイルは次回のチャット時に
`sync_local_kb()` が取り込む。
---
### GET /api/kb/file
1 ファイルの全文を返す。
**クエリパラメーター**
| 名前 | 必須 | 説明 |
|---|---|---|
| `path` | ✓ | ナレッジベースディレクトリからの相対パス(`web_search/xxxx.md` など) |
ナレッジベースの外を指すパス(`../` や絶対パス)、`.md` 以外、存在しないファイルはすべて `404`。
```json
{
"path": "fgo.md",
"name": "fgo.md",
"title": "Fate / Grand Order",
"content": "# Fate / Grand Order
…",
"size": 13942,
"updated_at": "2026-06-20T09:39:45.542875Z"
}
```
---
### GET /api/kb/embedding
埋め込みモデルの現状と、再埋め込みの進捗を返す。
```json
{
"model": "ollama/nomic-embed-text",
"dimensions": 768,
"file_count": 36,
"indexed": 36,
"stale": 0,
"running": false,
"total": 36,
"done": 36,
"failed": 0,
"started_at": "2026-08-23T13:50:23.196274Z",
"finished_at": "2026-08-23T13:50:29.951099Z",
"last_error": null
}
```
`indexed` は**現在のモデルで**埋め込まれている件数、`stale` は別モデル(またはモデル不明)のまま残っている件数。
`running` 以下は実行中の進捗で、プロセス内のメモリにしか無い(再起動で消える。ワーカーを複数立てるとワーカーごとに別の値になる)。
---
### POST /api/kb/embedding/test
短いテキストを実際に 1 回埋め込んでみて、モデルが応答するかを確かめる。失敗しても 200 で返し、`error` に理由が入る。
```json
{ "ok": true, "model": "ollama/nomic-embed-text", "dimensions": 768, "latency_ms": 412.3, "error": null }
```
---
### POST /api/kb/embedding/reindex
全ファイルの再埋め込みをバックグラウンドで開始し、開始直後の状態(`running: true`)を返す。
すでに実行中なら何も起こさず現在の進捗を返す。
---
## ログ
### GET /logs
LLM API 呼び出しログを `created_at` 降順で返す。
**クエリパラメーター**
| パラメーター | 型 | 説明 |
|---|---|---|
| `session_id` | string | セッションで絞り込み |
| `status` | `ok` / `error` | 成功・失敗で絞り込み |
| `limit` | int (≤500) | 最大件数(デフォルト 100) |
| `offset` | int | スキップ件数 |
**レスポンス**
```json
[
{
"id": 1,
"session_id": "uuid",
"model": "gpt-5.4",
"request_json": "[{\"role\":\"user\",\"content\":\"...\"}]",
"response_text": "...",
"error": null,
"latency_ms": 832.5,
"created_at": "2026-06-01T12:00:00"
}
]
```
---
### DELETE /logs/{log_id}
指定ログを削除。`204 No Content` を返す。
---
## ヘルスチェック
### GET /health
```json
{ "status": "ok" }
```