Difyのinputsでクライアントごとに表示を出し分ける

AI

はじめに

前回の記事では、DifyのテンプレートノードでHTMLタグを出力し、FAQチャットボットにボタンやフォームを表示する方法をまとめました。 Web App上で使う分には便利ですが、同じDifyアプリをSlack Botや独自Web UIからバックエンドAPIとして呼び出すと、表示まわりで困ることがあります。

たとえば、DifyのWeb Appでは<button><form>がインタラクティブなUIとして描画されます。 しかしSlackから利用する場合、そのHTMLをそのまま返しても期待したボタンや入力フォームにはなりません。 ユーザーにHTML断片が見えたり、本来押せるはずのボタンがただの文字列になったりします。

今回は、Difyを複数のクライアントから使う場合のベストプラクティスとして、inputsを使って表示内容を出し分ける方法を紹介します。 以前作成したDifyバックエンドAPIと連携するSlack Botや、SlackのスレッドでDifyとの会話を継続する記事にも足し込みやすい形です。

参考にした公式ドキュメントは以下です。

なぜinputsを使うのか

Difyのアプリは、Web Appとして公開することも、API経由でバックエンドサービスのように使うこともできます。 この性質を使うと、Dify上で組んだプロンプト、ナレッジ、ワークフローを複数の入口から再利用できます。

一方で、入口が増えると「返すべき表示形式」は揃わなくなります。 Web AppならHTMLフォームが使えますが、SlackならプレーンテキストやSlack側のUIに合わせる必要があります。 CLIやバッチ処理から呼ぶ場合は、装飾を減らした短いテキストのほうが扱いやすいこともあります。

ここで重要なのは、表示の出し分け条件をDifyへどう渡すかです。 いくつか方法はありますが、表示条件はinputsに入れるのが一番扱いやすいです。 inputsは、Difyアプリ側で定義した変数へ値を渡すための領域です。 そのため「この呼び出し元はSlackです」「HTMLフォームは使えません」といった情報を明示的に渡す用途に向いています。

逆に、表示条件をプロンプトの本文に混ぜたり、ユーザーIDの文字列から推測したりすると、後から条件が増えたときに管理しづらくなります。 inputsをクライアントとDifyの間の契約として決めておくと、テンプレートノードやIf-Elseノードからも扱いやすくなります。

ベストプラクティス

本記事で紹介する方針は、以下の1点です。

Difyの表示出し分けは、inputsclientと表示能力を渡して制御します。

具体的には、API呼び出し時に次のような値を渡します。

{
  "inputs": {
    "client": "slack",
    "output_format": "markdown",
    "supports_html_form": "false",
    "supports_quick_reply": "false"
  }
}

ここで渡している値は、Dify側の表示制御に必要な最小限の情報です。 clientで呼び出し元を示し、output_formatで返しやすい形式を示します。 さらに、supports_html_formsupports_quick_replyで、そのクライアントが対応できるUIを表します。

この形にすると、Dify側のテンプレートでは「SlackならMarkdown」「Web AppならHTMLフォーム」のように分岐できます。 また、後から独自Web UIやCLIを追加する場合も、同じ変数セットに値を足すだけで対応できます。

inputsの設計例

最初から細かく作り込みすぎると、Dify側の条件分岐が読みにくくなります。 まずは、以下の4つ程度から始めるのがおすすめです。

変数名 用途
client webappslackapi 呼び出し元の種類
output_format htmlmarkdownplain 返したい表示形式
supports_html_form truefalse HTMLフォームを返してよいか
supports_quick_reply truefalse クイック返信ボタンを返してよいか

Difyの入力変数は、Difyアプリの画面で事前に定義しておきます。 少しわかりにくいですが、ユーザー入力ブロックの「入力フィールド」から変数を追加できます。

変数の追加
変数の追加
変数の追加例
変数の追加例

APIから渡すinputsのキーは、その入力変数名と揃えます。

筆者としては、最初はclientだけで分岐してもよいと思います。 ただ、Web App以外のクライアントが増えそうなら、output_formatsupports_html_formを分けておくほうが後で楽です。

たとえば、同じwebappでも「Dify標準Web App」と「自作Web UI」では使えるUIが違うかもしれません。 その場合、clientだけで判断すると分岐が粗くなります。 表示能力を別の変数にしておくと、クライアント名に依存しすぎない設計にできます。

API呼び出し例

DifyのAPIドキュメントでは、completion-messageschat-messagesのリクエストでinputsを渡せます。 Web App向けの表示とSlack向けの表示を切り替えたい場合は、ここにクライアント情報を入れます。

チャットフローの場合は以下のようにユーザーの質問をqueryに入れ、会話を続ける場合はconversation_idも渡します。

curl --location --request POST 'https://api.dify.ai/v1/chat-messages' \
  --header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "inputs": {
      "client": "slack",
      "output_format": "markdown",
      "supports_html_form": "false",
      "supports_quick_reply": "false"
    },
    "query": "ログインできません",
    "response_mode": "streaming",
    "conversation_id": "",
    "user": "slack-user-123"
  }'

ここではuserも指定していますが、表示の出し分け条件としては使っていません。 表示条件はinputsに閉じ込め、userはAPI利用時に必要な利用者識別として扱います。 役割を分けておくと、Dify側のワークフローを読んだときに意図が分かりやすくなります。

Slack Botから渡す例

以前の記事では、Slack BotからDifyの/chat-messagesを呼び出しました。 その実装にinputsを足すなら、以下のような形になります。

async function callDifyFromSlack(params: {
  query: string;
  conversationId: string;
  teamId: string;
  channelId: string;
  userId: string;
}) {
  const resp = await fetch(`${process.env.DIFY_BASE_URL}/chat-messages`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.DIFY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      inputs: {
        client: "slack",
        output_format: "markdown",
        supports_html_form: "false",
        supports_quick_reply: "false",
        slack_team_id: params.teamId,
        slack_channel_id: params.channelId,
      },
      query: params.query,
      response_mode: "streaming",
      conversation_id: params.conversationId,
      user: `slack:${params.teamId}:${params.userId}`,
    }),
  });

  if (!resp.ok) {
    throw new Error(`Dify API error: ${resp.status} ${await resp.text()}`);
  }

  return resp;
}

ポイントは、Slack固有の情報と表示制御の情報を同じinputs内に置きつつ、名前で役割を分けることです。 clientoutput_formatはDify内の表示分岐に使います。 slack_team_idslack_channel_idは、必要に応じて回答内容の調整やログ用に使えます。

ただし、Dify側で使わない値をむやみに増やす必要はありません。 最初はclientoutput_formatだけでも十分です。 実際にテンプレートや条件分岐で必要になった値だけを追加すると、ワークフローが読みやすくなります。

Dify側で出し分ける

Dify側では、If-Elseノードかテンプレートノードでinputsの値を参照します。 前回の記事で使ったHTMLフォームを、Web AppとSlackで出し分ける例は以下です。

{% if supports_html_form == "true" %}
問い合わせが必要な場合は、以下のフォームから送信できます。
<form data-format="json">
<label for="category">問い合わせカテゴリ</label>
<input type="select" name="category" data-options='["ログイン","料金","操作方法","不具合","その他"]' />
<label for="detail">困っている内容</label>
<textarea name="detail" placeholder="どの画面で何が起きたかを書いてください"></textarea>
<button data-variant="primary">送信する</button>
</form>
{% elif output_format == "markdown" %}
問い合わせが必要な場合は、次の形式で返信してください。

カテゴリ: ログイン / 料金 / 操作方法 / 不具合 / その他
内容: 困っていること

例:
カテゴリ: ログイン
内容: パスワード再設定メールが届きません。
{% else %}
問い合わせが必要な場合は、カテゴリと困っている内容を返信してください。
{% endif %}

この例では、client == "slack"ではなく、supports_html_formoutput_formatで分岐しています。 クライアント名よりも表示能力で判断するほうが、後から別のクライアントを追加しやすいためです。

たとえば、将来Web App以外の独自UIがHTMLフォーム相当の表示に対応した場合、supports_html_formtrueにするだけで同じ分岐を使えます。 逆に、client == "webapp"で固定していると、新しいクライアント用の条件を足す必要があります。

クイック返信もinputsで制御する

前回の記事では、<button data-message="...">を使ってクイック返信ボタンを表示しました。 これもWeb Appでは便利ですが、SlackやCLIではそのまま使えません。

そのため、クイック返信もsupports_quick_replyで分岐すると扱いやすいです。

{% if supports_quick_reply == "true" %}
この回答で解決しましたか?
<button data-variant="primary" data-message="解決しました">解決しました</button> <button data-variant="secondary" data-message="もう少し詳しく教えてください">もう少し詳しく</button>
{% elif output_format == "markdown" %}
この回答で解決しましたか?

- 解決した場合は「解決しました」と返信してください
- 続けて確認したい場合は、そのまま質問を書いてください
{% else %}
この回答で解決したか、追加で確認したい内容があるかを返信してください。
{% endif %}

このようにしておくと、Dify Web Appではボタンを出し、Slackでは自然な案内文を返せます。 Slack側で本当にボタンを出したい場合は、Difyから返ってきたテキストをSlack Bot側でBlock Kitに変換する必要があります。 ただし、その場合でも「SlackはHTMLクイック返信に対応しない」という情報をinputsで渡しておくと、Dify側が余計なHTMLを返さずに済みます。

inputs設計の注意点

inputsは便利ですが、何でも入れればよいわけではありません。 Dify側の分岐で使う値に絞ることが大事です。

よくある失敗は、クライアント側で取得できる情報をすべてinputsに詰め込むことです。 Slackであれば、チームID、チャンネルID、ユーザーID、スレッドID、投稿時刻など多くの情報があります。 しかし、Dify側で使わない値まで渡すと、ワークフローの入力が増えすぎて見通しが悪くなります。

筆者なら、最初は以下の粒度にします。

目的 渡す値
表示形式の切り替え output_format
HTMLフォームの可否 supports_html_form
クイック返信の可否 supports_quick_reply
クライアント別の文面調整 client

また、値の表現も揃えておくと便利です。 たとえば、output_formathtmlmarkdownplainの3種類に固定します。 真偽値は、Dify側の扱いやすさに合わせてtrue/false相当の文字列にしておくと、テンプレートノードで比較しやすいです。

命名も重要です。 is_slackのように特定クライアントへ寄せた変数を増やすより、supports_html_formのように能力を表す名前にしたほうが再利用しやすくなります。 将来の入口が増えたときに、条件分岐を増やさずに済む場面が多くなります。

まとめ

DifyのWeb App向けにHTMLフォームやクイック返信ボタンを作ると、ユーザーの入力体験はかなり良くなります。 ただし、同じアプリをSlackや独自UIからAPI利用する場合は、そのHTMLがそのまま使えるとは限りません。

表示の出し分けは、inputsclientoutput_formatsupports_html_formsupports_quick_replyを渡して制御するのが扱いやすいです。 Dify側ではテンプレートノードやIf-Elseノードでこれらの値を見て、Web AppにはHTML、SlackにはMarkdownのように返す内容を切り替えます。

今回の作業で改めて感じたのは、inputsを単なる入力欄ではなく「外部クライアントとの契約」として設計すると、Difyアプリを長く使い回しやすくなることです。 まずはclientoutput_formatだけでもよいので、表示条件をinputsへ明示するところから始めると、前回作ったWeb App向けUIも壊さずにSlack連携へ広げられます。

コメント

タイトルとURLをコピーしました