DifyとLINEボットサーバーを連携してみる その3 conversation_id編

# はじめに
[前回](https://qiita.com/bathtimefish/items/63c00e637e8dc9b090fe)の続きです。DifyのAPIでは[conversation_idを使って継続的にエージェントとの会話を続ける](https://docs.dify.ai/guides/application-publishing/developing-with-apis#conversational-applications)ことができます。前回の応答に含まれる`conversation_id`をボットサーバーの中で一時保存することができれば、よりDifyのエージェント対話品質が上がりそうなのでやってみました。

# LINE Botサーバーをカスタマイズする

前回`routes/api/messaging.ts`を以下のようにアップデートしました

```messaging.ts
import type { Handlers, FreshContext } from "$fresh/server.ts";
import { messagingApi, MessageEvent } from "npm:@line/bot-sdk@9.2.2";
import type { ClientConfig, TextEventMessage } from "npm:@line/bot-sdk@9.2.2";

declare interface DifyChatMessageResponse {
  event: string;
  task_id: string;
  id: string;
  message_id: string;
  conversation_id: string;
  mode: string;
  answer: string;
  metadata: {
    usage: {
      prompt_tokens: number;
      prompt_unit_price: string;
      prompt_price_unit: string;
      prompt_price: string;
      completion_tokens: number;
      completion_unit_price: string;
      completion_price_unit: string;
      completion_price: string;
      total_tokens: number;
      total_price: string;
      currency: string;
      latency: number;
    }
  },
  created_at: number; 
}

declare interface DifyChatMessageRequest {
  // deno-lint-ignore ban-types
  inputs: {};
  query: string;
  response_mode: "blocking";
  user: string;
  conversation_id?: string;
}

declare type conversationIds = { userId: string, conversationId: string }[];
let conversationIds: conversationIds = [];
// deno-lint-ignore no-inferrable-types
let invervalId: number = 0;
const conversationCanselMin = 1;

const getConversationId = (event: MessageEvent): string|null => {
  const userId = event.source.userId;
  if (!userId) return null;
  if (conversationIds.length === 0) return null;
  for (const conversationId of conversationIds) {
    if (conversationId.userId === userId) {
      return conversationId.conversationId;
    }
  }
  return null;
}

const setConversationId = (event: MessageEvent, conversationId: string): void => {
  const userId = event.source.userId;
  let updated = false;
  if (!userId) return;
  if (invervalId) clearInterval(invervalId);
  for (let i = 0; i < conversationIds.length; i++) {
    if (conversationIds[i].userId === userId) {
      conversationIds[i].conversationId = conversationId;
      updated = true;
      break;
    }
  }
  if (!updated) {
    conversationIds.push({ userId, conversationId });
  }
  invervalId = setTimeout(() => {
    conversationIds = [];
  }, 1000 * 60 * conversationCanselMin);
};

const config: ClientConfig = {
    channelAccessToken: "YOUR_CHANNEL_ACCESS_TOKEN",
    channelSecret: "YOUR_CHANNEL_SECRET",
};
const client = new messagingApi.MessagingApiClient(config);

export const handler: Handlers =  {
  async POST(_req: Request, _ctx: FreshContext): Promise<Response> {
    const body = await _req.json(); 
    const event: MessageEvent = body.events[0];
    const textMessage = event.message as TextEventMessage;
    console.log(textMessage.text);
    // request to Dify API
    const requestData: DifyChatMessageRequest = {
      inputs: {},
      query: textMessage.text,
      response_mode: "blocking",
      user: "line-bot",
    };
    // get conversation id
    const conversationId = getConversationId(event);
    console.log(conversationId);
    if (conversationId) requestData["conversation_id"] = conversationId;
    const resp = await fetch("https://api.dify.ai/v1/chat-messages", {
      method: "POST",
      headers: {
	"Authorization": "Bearer ENTER-YOUR-SECRET-KEY",
        "Content-Type": "application/json",
      },
      body: JSON.stringify(requestData),
    });
    const res = await resp.json() as DifyChatMessageResponse;
    await client.replyMessage({
      replyToken: event.replyToken,
      messages: [
        {
          type: "text",
          text: res.answer,
        },
      ],
    });
    // set conversation id
    setConversationId(event, res.conversation_id);
    return new Response(null, { status: 204 });
  }
};
```

# 使ってみる

まず、コード更新前のBotの応答は以下のようになっていました。

![Screenshot_20240728-154734.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61995/eef93bb3-558f-04e3-df8e-445f32d02cc1.png)

アスナの方は容姿についての説明になっていません。

コードを更新したサーバーを実行します

```shell
deno task start
Task start deno run -A --watch=static/,routes/ dev.ts
Watcher Process started.
The manifest has been generated for 7 routes and 1 islands.

 🍋 Fresh ready 
    Local: https://localhost:8000/
```

![Screenshot_20240728-161805.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61995/6d5d4775-38da-7ac7-035d-f2739ba325f7.png)

アスナのほうも容姿について説明するようになりました。

# conversation_idの保存方法

conversation_idをサーバー内に保持するためのコードは以下です

```messaging.ts
const setConversationId = (event: MessageEvent, conversationId: string): void => {
  const userId = event.source.userId;
  let updated = false;
  if (!userId) return;
  if (invervalId) clearInterval(invervalId);
  for (let i = 0; i < conversationIds.length; i++) {
    if (conversationIds[i].userId === userId) {
      conversationIds[i].conversationId = conversationId;
      updated = true;
      break;
    }
  }
  if (!updated) {
    conversationIds.push({ userId, conversationId });
  }
  invervalId = setTimeout(() => {
    conversationIds = [];
  }, 1000 * 60 * conversationCanselMin);
};
```

LINEの`userId`とDifyの`conversationId`を対にして配列に保存して、一定時間経つと揮発するようにしています。揮発までの時間は`conversationCanselMin`で任意に変更できます。

配列から`conversationId`を取得するのは以下のコードです。

```messaging.ts
const getConversationId = (event: MessageEvent): string|null => {
  const userId = event.source.userId;
  if (!userId) return null;
  if (conversationIds.length === 0) return null;
  for (const conversationId of conversationIds) {
    if (conversationId.userId === userId) {
      return conversationId.conversationId;
    }
  }
  return null;
}
```

# おわりに

`conversation_id`を利用することで、前回よりも対話能力が高いボットサーバーにアップデートすることができました。Dify APIはシンプルで便利なのでLINE Botとの相性も良いと思います。

みなさんのDifyを利用したLINE Bot開発の参考になれば幸いです。