コンテンツにスキップ
HaneDOCSEN

アプリを作る

組み込みチャット

会話を Hane に保存し、メッセージごとに Agent を選んで頼み、進行を SSE で受け取る組み込みチャットの作り方です。

このページでは、アプリに「Agent に頼む」チャットを組み込む方法を説明します。会話とメッセージは Hane に保存され、利用者の文はチャットの Agent が受けて、必要な業務の Agent を呼びます。外部のチャットサービスの席の契約は要りません。

  1. 利用者が会話にメッセージを送ると、Hane はメッセージを保存し、同じトランザクションでチャットの Agent(定義で default: true の Agent)の AgentRun を始めます。
  2. チャットの Agent は、agents.list で呼べる業務の Agent を確かめ、agents.run で呼んで結果を受け取り、答えを返事にします。1 つの頼みで複数の業務の Agent を呼ぶこともあります。
  3. 足りない入力があれば、同じ会話で聞き返します。
  4. 応答の生成、ツールの開始と終了、承認待ちなどの変化は、会話のイベント(SSE)で届きます。再接続すると、履歴と現在の状態から続きを受け取れます。

外への操作は、AgentRun と同じく承認の Task に回ります。チャットの返事を承認として扱いません。

有効なバージョンに、チャットの Agent を 1 つだけ置きます。ツールは agents.list と agents.run だけで、呼んでよい業務の Agent を agents に並べます。

apiVersion: agents/v1alpha1
kind: Agent
metadata:
name: assistant
spec:
purpose: 頼みに合う業務の Agent を呼んで、その結果を答える
default: true
input:
type: object
required: [request]
properties:
request: { type: string, title: 頼まれた文, format: message }
documents: { type: string, title: 添付した文書, format: document }
driver: llm
instructions: |-
自分では業務をせず、agents.list で呼べる Agent を確かめ、agents.run で呼んで、結果を短く答える。
必要な値が文にも会話にもなければ、呼ばずに聞き返す。
tools: [agents.list, agents.run]
agents: [lease-qa, quote-request, vehicle-check]
output:
type: object
required: [status, answer]
properties:
status:
oneOf:
- { const: answered, title: 答えた }
- { const: needs_input, title: 足りない値を聞いた }
- { const: cannot, title: 頼めなかった }
answer: { type: string, maxLength: 2000 }

チャットの Agent はモデルのドライバー(llm・codex・claude)で動かします。チャットの Agent がない有効なバージョンでは、チャットは動きません。

  1. 会話を作る

    const conv = await unwrap(hane.POST("/v1/agents/conversations", { body: {} }));

    会話は作った利用者だけが読めます。題名を渡さなければ、最初のメッセージの 1 行目が題名になります。

  2. メッセージを送る

    const msg = await unwrap(
    hane.POST("/v1/agents/conversations/{id}/messages", {
    params: { path: { id: conv.conversation_id } },
    body: {
    content: "C-1001 の満了日を教えて",
    client_message_id: crypto.randomUUID(), // 送り直しでも同じ値を使う
    agent: "lease-qa", // 任意。このメッセージではこの Agent だけを呼ぶ
    },
    }),
    );
    // 202。msg.run_id はチャットの Agent の実行
    • agent を省くと、チャットの Agent が文から業務の Agent を選びます。
    • チャットの Agent が呼べない Agent を指定すると VALIDATION_FAILED になり、何も保存しません。
    • 返事が終わるまで、同じ会話の次のメッセージは CONFLICT になります。
    • 先にアップロードした文書を attachments: [{ document_id }] で 5 つまで添付できます。
  3. イベントを受け取る

    const res = await hane.GET("/v1/agents/conversations/{id}/events", {
    params: { path: { id: conv.conversation_id }, ...(lastId ? { header: { "Last-Event-ID": lastId } } : {}) },
    parseAs: "stream",
    });
    // res.data は ReadableStream。再接続では最後に受け取った id を Last-Event-ID に渡す

イベントは text/event-stream で届きます。

event data
snapshot 会話の現在状態。conversation・items(メッセージ)・turns(実行とその ActionRequest)・activities(進行記録)・cursor
failure 接続中の拒否(status・code・message)。接続は閉じる
  • 最初に現在状態の snapshot が届き、その後は変化があるたびに届きます。SSE の id は cursor です。
  • activities は、Last-Event-ID より後の進行記録です。再接続しても同じ記録を二重に受け取りません。
  • 15 秒ごとにコメントの heartbeat が届き、45 秒以内に接続が閉じます。再接続のたびに、認証と閲覧の権限を確かめ直します。
  • 推論の内部の過程、資格情報、モデルの内部の JSON は届きません。

進行記録の kind は次のとおりです。message.accepted・agent.queued・agent.started・tool.started・tool.completed・tool.failed・approval.pending・action.updated・agent.completed。

ブラウザは Hane を直接呼ばないので、BFF がイベントの接続を中継します。参照アプリは、ブラウザの Last-Event-ID を Hane に渡し、ブラウザが切断したら上流の接続も止めています。中継の応答には content-type: text/event-stream・cache-control: no-cache, no-transform を付け、途中で溜めないようにします。

UI ライブラリ @hane/ui の AgentChat は、会話の一覧、入力欄(/ で Agent を指定、添付、送信の取り消し)、手順の表示、承認と結果のカードを持つ部品です。アプリのサーバーの呼び方は ChatTransport で渡します。次の例の bff は、BFF の /bff/* を呼ぶアプリの関数です。

import { AgentChat, type ChatTransport, subscribeEventSource } from "@hane/ui";
const transport: ChatTransport = {
createConversation: () => bff("/agents/conversations", { method: "POST", body: {} }),
sendMessage: (id, body) => bff(`/agents/conversations/${id}/messages`, { method: "POST", body }),
subscribe: (id, listener) => subscribeEventSource(`/bff/agents/conversations/${id}/events`, listener),
cancelRun: (id) => bff(`/agents/runs/${id}/cancel`, { method: "POST" }),
};
<AgentChat agents={agents} conversations={conversations} conversationId={id} onOpen={open} transport={transport} />;

transport は描くたびに作り直さず、同じものを渡します(作り直すと接続し直します)。approvalTasks・decideApproval を渡すと、自分が決められる承認をカードで決められます。決定は Task の完了として記録されます。