🧐

Difyのセルフホストで無理やりクエリパラメータをチャットフローに渡してみた

に公開

前置き

かなり無理やり、かつ出来るケースも必要なケースも限定的なため、あくまで「ふーん、やってみたんだね。」という感覚で見ていただければと思います。

検証環境

  • Dify version: 1.2.0

ことの発端

Difyのチャット画面のリンクにクエリパラメータを付けて、それをチャットフローの会話変数で保持することって出来ないのかな?と。ふと思ったのが発端です。

例:https://ドメイン/chat/Nccw0q7AFSPw77MT?param=123

システム構成

アーキテクチャ

DifyはAWSを利用してセルフホストしています。

参考リポジトリ(Difyのセルフホスト用)

https://github.com/sonodar/dify-aws-terraform
https://github.com/aws-samples/dify-self-hosted-on-aws

さらにreverse proxyとしてnginxと、ログ出力およびデータの取得・加工用サーバーとしてHonoを、dify-apiのコンテナのサイドカーとして
同じサービスに起動しています。

Difyのアプリケーションのパス

https://ドメイン/chat/Nccw0q7AFSPw77MT

ここにクエリパラメータを付与し、これをチャットフローのhttpノードで取得します。

https://ドメイン/chat/Nccw0q7AFSPw77MT?param=123

主要なワード

  • 会話変数
  • 会話ID

Difyの重要な概念

会話変数とは

Difyのチャットフローでは、値を変数に格納して保持する「会話変数」機能があります。

  • 会話毎に管理される
  • "Start New chat"で新しい会話を始めると初期化される
  • 今回はクエリパラメータの値をここに格納したい

会話ID

Difyのアプリケーションには会話IDというものがあります。これは各会話に一意に付けられるIDであり、ブラウザで画面を閉じたりしても、この会話IDを元に履歴を取得しているように見えます。Difyのチャットアプリ左側の会話履歴一覧の一つ一つ個別に一意に設定されています。

重要なポイント

クエリパラメータの値を会話ID毎に、会話変数に格納するためには、ブラウザからapiのコンテナ(reverse proxy)への通信の際、同時に会話IDとクエリパラメータを含んだパスを参照できる必要があります。

Difyの会話IDはいつ生成されるのか

会話IDはDifyのチャットフロー内の標準機能で参照できます。
変数名:{#sys.conversation_id#}

これはどのタイミングで生成されているのかを確認しました。結論から言うと、会話IDが生成されるのは初回メッセージを送った後です。

まずStart New chatをクリックした際に、新しい会話が開きます。このときのリクエストを検証ツールで確認すると次のリクエストが発生しています。

これらのリクエストのパラメータやヘッダーを見ますが、conversation_idというパラメータはどこにも見当たりません。

次に初回メッセージを送った際のリクエストを確認します。chat-messagesのリクエストがチャットメッセージを送った際のリクエストです。

このときconversation_idはまだ空の状態です。

同じリクエストのResponseを確認してみると、ようやくconversation_idが確認できました。

よって、会話IDが生成されるのはユーザが初回のメッセージを送った後となります。

クエリパラメータをいつ保持するか

初回メッセージを送った後に/nameのリクエストが実行されます。これは挙動から予想するに、会話履歴のタイトルを設定するリクエストと考えられます。

このリクエストを見ると、次のことが分かります:

  1. Request URLに、会話ID(conversation_id)が含まれている
  2. refererに、ブラウザで表示しているURLが設定されている

このリクエストを使えば、会話IDをkeyとして、クエリパラメータを保持できそうです。

実際にやってみる

値をRedisにsetする

ECSで起動しているdify-apiのサービスをおさらいします。dify-apiのコンテナと、サイドカーとしてreverse_proxy、およびデータの取得・加工用サーバーを起動しています。

  • /nameのリクエストが実行された際に、リクエストurlから会話IDを抽出する
  • refererからクエリパラメータを抽出する

これらをkey, valueとしてRedisに保存します。

 {conversation_id, param}
verification.ts
/*
コードはあくまで一例で、ALBで付与されるHTTP_X_AMZN_OIDC_DATAを
他の用途で利用しているためoidcHeaders等を利用しています。
*/

// request: https://ドメイン/api/conversations/47916170-1840-482d-8868-9a14fe2cb90e/name
// referer: https://ドメイン/chat/Nccw0q7AFSPw77MT?param=123
async function setRedis(originalUri: string, oidcHeaders: OidcHeaders) {
  // refererからparamクエリパラメータを取得(originalUriが/nameで終わる場合のみ)
  let param = null;
  if (oidcHeaders.referer && originalUri && originalUri.endsWith('/name')) {
    try {
      const url = new URL(oidcHeaders.referer, 'http://dummy.com');
      param = url.searchParams.get('param');
      if (param) {
        // originalUriからconversation IDを抽出してキーとして使用
        const conversationMatch = originalUri.match(/\/conversations\/([^\/]+)\/name$/);
        const conversationId = conversationMatch ? conversationMatch[1] : null;
        console.log(`DEBUG LOG SET REDIS KEY = ${conversationId}`);
        console.log(`DEBUG LOG SET REDIS VALUE = ${param}`);
        
        if (conversationId) {
          // Difyで取得してすぐに会話変数にセットするので、すぐに消して問題ない。
          redisClient.setNX(conversationId, param).then(
            () => redisClient.expire(conversationId, 30)
          );
        }
      }
    } catch (error) {
      console.error('URL解析エラー:', error);
    }
  }
}

値をRedisからgetするエンドポイント

verification.ts
// param取得のエンドポイント
app.get('/api/verification-service/fetch-param/:conversation_id', async (c: any) => {
  const conversationId = c.req.param('conversation_id');
  console.log(`DEBUG LOG CONVERSATION ID: ${conversationId}`);

  let param: string | number = 0;

  if (!conversationId) {
    console.log('DEBUG LOG no conversation_id');
  } else {
    try {
      const fetchedParam = await redisClient.get(conversationId);
      console.log(`DEBUG LOG PARAM: ${fetchedParam}`);
      param = fetchedParam == null ? 0 : fetchedParam;
    } catch (error: any) {
      console.error('Redis取得エラー: ', error);
    }
  }
  
  return c.text(param.toString());
});

DifyのHTTPリクエストノードでGETリクエストを作成します:

https://ドメイン/api/verification-service/fetch-param/{{#sys.conversation_id#}}

HTTPリクエストノードの次のノードで、レスポンスで取得したparamを会話変数にセットしてあげます。
これで会話IDをkeyとしてクエリパラメータを取得し、会話変数に保持する仕組みができます。

まとめ

限定的な使い方になりますが、以上でクエリパラメータを会話ID毎に保持し、Difyのチャットフロー内で取得することができました。

nginxの設定や、AWS内の細かい設定は省いています。そのためかなり分かり辛い内容であるため、私自身の記憶の整理と、一つの方法としてある、という捉え方でいてくれると嬉しいです。

DELTAテックブログ

Discussion