カスタムチャットUIの構築

このページは機械翻訳により提供されています。翻訳内容と英語版に相違がある場合は、英語版が優先されます。

このガイドでは、Headless API上でエンドツーエンドのカスタムチャットインターフェースを構築する方法を示します。 UIを構築するには、エンドポイントの呼び出し、認証情報を安全に保持するバックエンド、Server-Sent Events(SSE)ストリームをライブで信頼できる会話に変換するフロントエンドが必要です。

エンドポイントの詳細についてはHeadless APIリファレンスを、リクエストシーケンスについてはカスタムインターフェースAPIウォークスルーを、このガイドと併せて使用してください。

ビルドの概要

このガイドを使用して、認証情報を保持してリクエストをプロキシするバックエンドと、Genieとの会話をライブカードとメッセージバブルにストリーミングするブラウザーフロントエンドを構築します。 次の順序で構築します。

各ステップは前のステップを基に構築されます。 ステップ2のレンダリングループが中核です。以降の各ステップは、このループがディスパッチするハンドラーです。

ステップ1: ブラウザーとAPIの間にバックエンドを配置する

ブラウザーからHeadless APIを直接呼び出さないでください。 ブラウザーが通信する小規模な同一オリジンのバックエンドを立ち上げ、そのバックエンドからWorkatoに通信させます。 これにより、認証情報を保護し、クロスオリジン(CORS)の問題を回避できます。

  • クライアント側コードでDeveloper APIトークン(wrkaus-…)を絶対に公開しないでください。 これはプロビジョニングにのみ使用されるビルダーシークレットです。 ブラウザーに見せてよいのは、公開OAuth client_idとエンドユーザー自身のOAuthアクセストークンだけです。
  • OAuthトークン交換をバックエンド経由でリレーします。 ブラウザーはPKCEフローを実行して認可codeを受け取り、その後バックエンドがid.workato.com/oauth/tokencode(後でrefresh_token)を交換します。 これにより、トークン処理をサーバー側に保持し、IDホストへのクロスオリジンリクエストを回避できます。
  • APIキー連携では、バックエンドで認証を注入します。 バックエンドはAPIキーを保持し、各ランタイムリクエストにAuthorizationヘッダーとX-IDP-User-Idヘッダーを追加してから、SSEストリームをブラウザーにプロキシします。

ユーザー向けUIにはOAuthを選択

実際のユーザーがサインインするUIには、OAuth 2.0(PKCE)を使用します。 APIキー認証ではすべての呼び出し元に単一の静的キーを使用するため、ブラウザーではなく、信頼できるサーバー間バックエンドにのみ適しています。 認証を参照してください。

ステップ2: ストリームを開いてレンダリングループを実行する

ユーザーがメッセージを送信すると、バックエンドはstream: trueを指定してメッセージを送信を呼び出し、SSEレスポンスをブラウザーにプロキシします。 フロントエンドはそのストリームを読み取り、各イベントをハンドラーにディスパッチします。 このループがUIの中核であり、それ以外はすべてこのループが呼び出すハンドラーです。

ストリームを行単位で読み取り、現在のevent:タイプを追跡し、各data:行をJSONとして解析して、そのタイプに基づいてディスパッチします。 空行でイベントが区切られます。 例:

js
// `response` is the proxied SSE stream from your backend.
let currentEvent = null;
for await (const line of readLines(response.body)) {
  if (line.startsWith("event:")) {
    currentEvent = line.slice(6).trim();
  } else if (line.startsWith("data:")) {
    dispatch(currentEvent, JSON.parse(line.slice(5).trim()));
  }
}

function dispatch(type, data) {
  switch (type) {
    case "processing.started":               showTypingIndicator();        break;
    case "skill.running":
    case "skill.completed":
    case "skill.failed":                      updateSkillCard(data);        break;
    case "skill.confirmation_required":       renderApprovalCard(data);     break;
    case "runtime_connection.auth_required":  renderConnectionCard(data);   break;
    case "agent.message":                     renderMessage(data);          break;
    case "processing.finished":               endTurn();                    break;
    default:                                  break;  // ignore system.ping and unknown types
  }
}

次の表は、各イベントとUIでの処理内容を対応付けています。 以降のステップで各ハンドラーを実装します。

イベント含まれる内容UIで行うこと
processing.started入力中インジケーターを表示します
skill.
running / skill.
completed / skill.
failed
skill
_name, skill_id
skill_nameをキーにしてスキルカードを作成または更新します
skill.
confirmation
_required
call_id, skill_name, skill
_parameters
パラメーターとApproveボタンおよびRejectボタンを含む承認カードを表示します
runtime_connection
.auth_required
runtime_connection
_attempt_id, auth_link
auth_link.urlを開き、完了をポーリングします(ステップ5を参照)
agent.messagemessage_id, messageチャットバブルをレンダリングします。 message_idで重複排除し、Markdownをレンダリングします
processing.
finished
ターンを終了し、入力中インジケーターを削除します
system.ping無視します。 これは、長時間のターンまたは一時停止中のターンで送信されるキープアライブです

不明なイベントタイプを無視

上記のdefaultケースと同様に、認識しないイベントタイプはすべてno-opとして扱います。 これにより、UIの前方互換性を保ち、system.pingなどのキープアライブイベントを吸収できます。

ステップ3: スキルイベントを処理する

Genieが実行する各スキルについて、ライフサイクル全体でその場で更新される単一のカードをレンダリングします。 たとえば、イベントごとに新しい項目を追加するのではなく、runningからcompletedまたはfailedへ更新します。

  • カードのキーにはcall_idではなくskill_nameを使用します。 skill.runningskill.completedには通常、call_idが含まれず、skill_nameskill_idrecipe:<numeric_id>形式)だけが含まれます。 call_idが含まれることが保証されているのはskill.confirmation_requiredだけです。
  • 並列ツール呼び出しを処理します: Genieが複数のスキルを同時に実行する場合、受け取るskill.runningは1つですが、各スキルに対してskill.completedを受け取ります。 既存のカードがないskill_nameに対してskill.completedまたはskill.failedが届いた場合は、すべての呼び出しが表されるようにカードを作成します。 runningイベントが必ずターミナルイベントの前に発生すると想定しないでください。 一部のランタイムバージョンでは、skill.completedの代わりにskill.stoppedも発行されます。これはターミナル成功として扱ってください。
  • skill.completedの結果をレンダリングしないでください: このイベントには構造化されたresultは含まれません。 Genieは結果を内部でモデルに渡し、次のagent.messageに反映します。 フィードバックとしてカードがcompletedに遷移することを表示し、データはエージェントメッセージで伝えるようにします。 たとえばグラフを描画するために構造化出力が必要な場合は、Developer APIを通じてレシピのジョブ出力をサーバー側で取得します。 詳細については、スキルIDを関連付けるを参照してください。

ステップ4: 承認を処理する

スキルに確認が必要な場合、Genieはskill.confirmation_requiredを発行し、ターンを一時停止します。 スキルカードをawaiting状態でレンダリングし、展開します。

  • イベントのskill_parametersフィールドから解決済みのパラメーターをキー/値リストとして表示し、ユーザーがGenieの送信内容を確認できるようにします。
  • 小さなインラインアイコンではなく、視覚的に十分に目立つApproveボタンとRejectボタンを追加します。
  • イベントのcall_idを渡して、スキルを承認または拒否でリクエストを解決します。 解決をポストすると、同じストリームが再開します。
  • 承認カードは一度だけレンダリングし、その場で更新します。 UIがSSEストリームを開いたままにするのではなくイベントをポーリングする場合、ポーリングのたびにターンを再構築しないでください。 一時停止中のほとんどのポーリングではハートビートのみが返され、カードを継続的に再作成するとApproveボタンのクリックが失われる可能性があります。 コンテンツに変更がない場合は再レンダリングをスキップするか、イベントリストの再レンダリングで上書きされない独自のノードにカードをレンダリングします。

ユーザーが拒否すると、Genieの次のagent.messagerejection_reasonをそのまま引用する場合があります。 ユーザーの言葉を会話にエコーバックしたくない場合は、汎用的な理由を送信するか、理由を送信しません。

ステップ5: コネクションリクエストを処理する

スキルがアップストリームシステムに対するエンドユーザー自身の認証情報(Verified User Access)を必要とする場合、Genieはruntime_connection.auth_requiredを発行し、ターンを一時停止します。 イベントには、runtime_connection_attempt_idと認証URLを含むauth_linkの両方が含まれます。

  • auth_link.urlを開くボタン(例: Connect to your account)を含むカードをawaiting状態でレンダリングします。 ボタンをクリック可能な状態に保つために、承認カードと同じく、一度レンダリングしてその場で更新するルールを適用します。 リンクの有効期限が切れている場合は、runtime_connection_attempt_idを使用してランタイムコネクションリンクを取得で新しいリンクを取得します。
  • コネクションの完了を検出するには、statusauthorizedになるまでランタイムコネクションリンクを取得をポーリングするか、stateskill_processingでなくなるまで会話を取得をポーリングします。
  • 開いているSSEストリームは、ユーザー認証後に再開されたターンを配信しませんsystem.pingのみを発行し、その後system.stream_interruptedを発行します。 コネクションが完了したら、再開された返信をメッセージ履歴から読み取ります(message_idで重複排除)。ステップ7を参照してください。

ユーザーは帯域外でログインを完了し、多くの場合ストリームのアイドルウィンドウより時間がかかるため、ここではストリームの切断を想定内として扱います。ターンが失敗したと想定するのではなく、永続化された状態から復旧します。

ステップ6: エージェントメッセージをレンダリングする

agent.messageイベントをチャットバブルとしてレンダリングし、次の2点に対応します。

  • message_idで重複排除する: 再接続時に、同じメッセージがSSEストリームとメッセージ履歴エンドポイントの両方に届くことがあります。 確認済みのmessage_id値のセットを保持します。
  • Markdownをレンダリングする: メッセージテキストはほとんどの場合Markdownであり、太字、リスト、リンク、ときどきコードが含まれます。 Markdownライブラリまたは小さなインラインレンダラーでレンダリングします。 バブルスタイルでwhite-space: pre-wrapを使用している場合は、ブロック要素が正しくレイアウトされるように、Markdownコンテナでwhite-space: normalを設定します。

入力中インジケーターをメッセージリストの下部に固定します。追加するスキルカードの下に再固定し、ユーザーが自分のメッセージ、ライブのスキルアクティビティ、インジケーターを置き換える返信の順に確認できるようにします。

ステップ7: 状態を追跡して復旧する

最新イベントから導出し、再接続時には会話を取得にフォールバックして、会話全体の状態(例: ステータスピル)を表示します。 会話のstateは、idleai_runningskill_processingawaiting_approvalのいずれかです。 ランタイムコネクションの一時停止ではstateskill_processingのままです。awaiting_approvalskill.confirmation_requiredによる一時停止を示します。

ライブストリームだけが唯一のソースであると想定せず、リロードやストリーム切断時には永続化されたデータから会話を再構築します。 会話を再構築するには、次の手順を実行します。

1

メッセージ履歴と、イベントを取得から永続化されたイベントを取得します。

2

2つのリストをcreated_atでマージして並べ替えます(イベントには実行ごとのseq_numも含まれます)。

3

message_idでメッセージを重複排除し、マージしたリストをステップ3~6と同じハンドラーで再生します。

skill.*processing.*を含むすべてのイベントタイプは24時間保持されるため、イベントを取得から完全なタイムラインを再構築できます。 1つの例外は、ランタイムコネクション(Verified User Access)の一時停止後に再開されたターンです。再開されたイベントはイベントを取得には含まれないため、代わりにメッセージ履歴からその返信を復旧します。 会話タイムラインを再構築するを参照してください。

まとめ

完全なカスタムチャットUIには、次の順序で4つのコンポーネントがあります。

  • 認証情報を保持し、リクエストをプロキシするバックエンド
  • SSEストリームをディスパッチするレンダリングループ
  • カードとバブルを駆動する、イベントタイプごとのハンドラー
  • 永続化されたデータから再構築する復旧

ビルドが想定どおりに動作することを確認するには、メッセージを送信し、スキルカードが表示されてagent.messageがレンダリングされることを確認し、確認が必要なスキルを承認してから、ターンの途中でリロードし、タイムラインが再構築されることを確認します。

次の3つの不変条件は改めて述べる価値があります。いずれも単純なビルドが破綻しやすい箇所です。

  • ターミナルスキルイベントではcall_idが省略されることが多いため、スキルカードのキーにはskill_nameを使用します。
  • processing.finishedをターンの終了として扱い、不明なイベントタイプは無視します。
  • リロード時には、イベントを取得メッセージ履歴から再構築します。 ライブストリームを唯一のソースとして扱わないでください。

サーフェス間でスキルIDを関連付ける

同じスキルでも、参照する場所によって識別方法が異なります。これは、たとえばスキルの構造化出力をサーバー側で取得するために、ライブツール呼び出しをスキルまたはジョブレコードに関連付ける場合に重要です。

サーフェススキル識別子
Headless SSEイベントrecipe:<numeric_id>としてのskill_id
Developer APIスキル(/api/agentic/skillsskl-…ハンドル。数値のレシピIDがprovider_idです
Developer APIレシピ(/api/recipes/:id数値のレシピID

可能な場合はskill_nameで照合します。これはサーフェス間で最も信頼性の高いキーです。 SSEのskill_idをスキルレコードにマッピングするには、recipe:プレフィックスを削除し、数値IDをスキルのprovider_idと照合します。

最終更新日: