アーカイブ済み(2026-09-18): 本ドキュメントは../ui-brain-protocol.mdとして再設計中(Issue #8)。単一UI(音声UI)の要請から場当たり的に決まっていた接続方式・セッションモデルを、複数UI層を見据えて見直すため移動した。エンドポイント契約・役割分担・
request_id冪等性など、既に合意済みで引き継がれる部分も多い。今後の参照・更新は新ドキュメントを使うこと。本書は議論の経緯を追う場合の参考としてのみ残す。
ステータス: 一部実装済み(下記「実装状況」参照)
本書は、Windows側の音声UI(AiChat)・WSL側のBrain(AiChatWls)間で交わされるHTTP APIの契約を定義する。UI固有(音声UI・将来のテキストUI等)の実装は、本書を土台とした付録文書で定義する。現在の付録: voice-ui-spec.md(音声応答UI)。
過去の中間検討資料はarchive/を参照。本書はそれらを整理・統合した正本。
control、後述)の解釈・実行は、UI層の責務とする。この役割分担自体はwsl-brain-separation-plan.mdの「分離の境界」で既に合意済みのものを踏襲する。本書はそのAPI契約部分を実装可能な粒度まで詳細化したもの。
| メソッド・パス | 用途 |
|---|---|
POST /ask |
1回のユーザー入力に対する応答を得る |
POST /session/{session_id}/keepalive |
セッションの生存を通知する(3節参照) |
POST /clear_history |
セッションの会話履歴を消去する |
GET /health/live |
プロセスが生きているかの疎通確認 |
GET /health/ready |
Ollama等、依存先まで含めた疎通確認 |
session_id)session_idを1つ生成し(例: uuid4())、プロセスの生存期間中ずっと使い回す。プロセス再起動時は新しいIDに切り替わり、以前の履歴を引き継がない。session_idごとに会話履歴・OllamaChatインスタンスを保持する。SESSION_TTL_SEC)以上経過したセッションを回収してよい。発話の有無に関係なく、ハートビートが届いている間は保持する。処理中(ロック保持中)のセッションは、その回のスイープでは回収しない。SWEEP_INTERVAL_SEC=5分間隔でBrainが掃除する前提に対し、UI側は60秒程度を目安に)POST /session/{id}/keepaliveを呼ぶ。ただし本書はこのハートビート送信ループ自体の実装は必須としない(実装状況参照)。1回限りの用途(比較試験スクリプト等)はハートビートを送らなくてよく、いずれ上記TTLで回収される。client_type"voice" または "text"を指定する。音声UI向けの簡潔な口調・URL除去は、client_typeの値でBrainが切り替える(Brain側が呼び出し元を決め打ちしない)。未知の値は400エラーとする。将来のテキストUI(Web UI等)は"text"を送ればよく、Brain側の変更は不要。
POST /ask{
"session_id": "string",
"request_id": "string",
"text": "string",
"client_type": "voice | text"
}
request_id: 1回のユーザー入力(発話・テキスト送信)につき1つ生成し、その入力に対する再送(6節)では同じ値を使い回す。新しい入力には新しいrequest_idを生成する。{
"conversation": "string",
"control": [ { "type": "string", "...": "..." } ]
}
conversation: 読み上げ・表示すべき自然文。0文字あり得る(何も言うことがない場合。エラーではない)。control: UI層が解釈・実行すべきアクションの配列。0件以上。controlの一般規則Brainはデバイス状態を一切持たないため、controlの各要素は「意図・方向」だけを表現し、具体的な数値・現在値は含まない(現在値をUIからBrainへ送る必要をなくすため)。個々のtypeが何を意味するかは、本書ではなく、それを解釈するUI固有の付録文書で定義する(例: 音声UIのadjust_volumeはvoice-ui-spec.md参照)。
/ask呼び出し内で、tool-callingの複数ラウンドや後述の再試行によって同じtypeのアクションが複数回発生しても、Brainはcontrolに同じtypeを1件しか含めない(重複除去はBrain側の責務)。typeや不正な内容を例外にせず無視してよい(ログに残すことを推奨)。conversationが空文字でcontrolも空の場合のみ、Brainは「うまく答えられませんでした。もう一度お願いします。」のような定型の失敗文言をconversationに設定する。controlが非空なら、conversationが空でもこの定型文で上書きしない(アクションは成功しているのに失敗したと誤って伝えることになるため)。request_idによるリトライ安全性)/askは冪等ではない。呼び出すたびに、Brain側の会話履歴に追記し、controlに対応するclient action(音量変更など)も実行済みとして扱われる。
UIがタイムアウトやネットワーク断で応答を受け取れなかった場合、Brain側では次のいずれかが起きている可能性がある。
3番のケースで単純に同じ入力を再送すると、Brain側で同じ入力がもう一度処理され、会話履歴の二重追加やcontrolの二重適用が起こる。
session_idごとに、直近に処理したrequest_idとその結果を保持する。request_idが保持している値と一致する場合、OllamaChat.ask()を再実行せず、保持している結果をそのまま返す。一致しない場合は通常通り処理し、結果を新しいrequest_idと共に保持し直す。lock(3節)により同一セッションへのリクエストは直列化されているため、「処理中に別リクエストが同時に同じrequest_idを処理してしまう」競合状態は発生しない(2番のケースで届いた再送は、ロック取得まで待たされた後、この一致判定によってそのまま前回結果を受け取る)。last_resultは直近1件だけを保持すればよい。既存のセッション掃除処理(TTL)の対象にもそのまま含まれる。server.py、設計)class AskRequest(BaseModel):
session_id: str
request_id: str
text: str
client_type: str
@dataclass
class Session:
chat: OllamaChat
lock: threading.Lock = field(default_factory=threading.Lock)
last_heartbeat: float = field(default_factory=time.monotonic)
last_request_id: str | None = None
last_result: AskResult | None = None
@app.post("/ask", response_model=AskResponse)
def ask(request: AskRequest) -> AskResponse:
if request.client_type not in ("voice", "text"):
raise HTTPException(status_code=400, detail="client_type must be 'voice' or 'text'")
session = _get_or_create(request.session_id)
with session.lock:
session.last_heartbeat = time.monotonic()
if request.request_id == session.last_request_id and session.last_result is not None:
result = session.last_result
else:
result = session.chat.ask(request.text, client_type=request.client_type)
session.last_request_id = request.request_id
session.last_result = result
return AskResponse(conversation=result.answer, control=result.control)
request_idのUUIDと機能的には同等。本設計では1セッションにつき常に高々1件しか処理中にならない(セッションのlockで直列化済み)ため、順序性を利用した追加の検知をする必要がなく、カウンタの永続化・セッション再生成時のリセットを気にしなくていいUUIDの方がシンプルと判断した。以下は本書の対象外であり、各UIの付録文書がUI固有の判断として定義する。
本書が提供するのは、これらの判断をUI側が安全に行えるようにする土台(request_idによる冪等な再送)だけである。
POST /askが{conversation, control}を返す変更: AiChatWls側 実装・テスト済み・push済み(src/keina_assistant/server.py, llm.py, tools.py)。request_idによる冪等性(6節): 未実装(本書で設計のみ)。