LINE WORKS API x LLM を使ってチャットから予定調整してみる

# はじめに

近年、ChatGPTの登場により、大規模言語モデル（LLM）などのAI技術が注目を集め、文章生成にとどまらず、より複雑なタスクを処理できるAgent[[open-interpreter](https://github.com/KillianLucas/open-interpreter), [babyagi](https://github.com/yoheinakajima/babyagi)]も登場しています。
日常生活や業務をサポートしてくれる自分専用のAgentを持てるようになる日も現実的なものとなってきました。
そこで、今回普段業務を行う上で、実用的なアプリケーションである予定調整をLINE WORKS API と LLM を利用して試してみることにしました。
今回LLMには、OpenAI APIを使用しました。


# 実現したいシナリオ

業務時に起こり得る以下のユースケースをLINE WORKS API x OpenAI API(Function Calling)の力を借りて実現したいと思います。

- 参加者共通の空き時間を探して、カレンダーに予定を作成する
- 空き時間がなかった場合に、参加者が最も参加可能な時間を見つけて予定を作成する

## OpenAI Functions Agent

Function Calling自体の説明は省略しますが、今回の実装では、LangChainに実装されてある[OpenAI Functions Agent](https://python.langchain.com/docs/modules/agents/agent_types/openai_functions_agent)を利用して予定調整機能を実現しています。

https://python.langchain.com/docs/modules/agents/agent_types/openai_functions_agent

## Functions

予定調整機能を実現するためにFunction Callingに登録する以下の関数(Function)を実装しました。

- get_events
  - 指定したユーザーの予定を取得する。
  - 使用LINE WORKS API
    - [基本カレンダーの予定リストの取得](https://developers.worksmobile.com/jp/docs/calendar-default-event-user-list)
- create_event_with_attendees
  - 予定を作成する。複数ユーザーの指定がある場合は、[参加者リスト](https://developers.worksmobile.com/jp/docs/calendar-default-event-user-create#Attendee)に追加する。
  - 使用LINE WORKS API
    - [基本カレンダーの予定登録](https://developers.worksmobile.com/jp/docs/calendar-default-event-user-create)
- find_common_free_times
  - 指定したユーザー間で共通の空き時間を調べる。
  - 使用LINE WORKS API
    - [基本カレンダーの予定リストの取得](https://developers.worksmobile.com/jp/docs/calendar-default-event-user-list)
- find_bottleneck
  - 指定したユーザー間で予定の重なりを調べ最も予定が重なっていない時間帯を求める。
  - 使用LINE WORKS API
    - [基本カレンダーの予定リストの取得](https://developers.worksmobile.com/jp/docs/calendar-default-event-user-list)
- send_message_to_user
  - 指定したユーザーにメッセージを送信する。
  - 使用LINE WORKS API
    - [メッセージの送信 - ユーザー指定](https://developers.worksmobile.com/jp/docs/bot-user-message-send)

## Agent構築

用意した関数をAgentに登録します。コードの一部を以下に抜粋します。


```python
# 関数からAgentが利用可能なToolに変換する
# args_schemaにスキーマを指定することで以下の恩恵が受けれます。
# 1つ目は、Agentからどのような情報が必要かを伝えること。
# 2つ目の仕事は、Toolの内部機能を実行する前に、それらの入力を検証すること。
tool_get_events = StructuredTool.from_function(
    get_events,
    args_schema=EventUserListInput,
)

# 予定取得のスキーマ
class EventUserListInput(BaseModel):
    user: str = Field(..., description="予定を取得したいユーザ名。")
    from_date_time: str = Field(
        ...,
        description="予定開始時刻 **YYYYY-MM-DDThh:mm:ssTZD** の形式に従う必要がある。 URL encoding. (+ → %2B)",  # noqa
    )
    until_date_time: str = Field(
        ...,
        description="予定終了時刻 **YYYYY-MM-DDThh:mm:ssTZD** の形式に従う必要がある。 URL encoding. (+ → %2B)",  # noqa
    )

# 〜省略〜

tools = [
    tool_get_events,
    tool_create_event_with_attendees,
    tool_find_common_free_times,
    tool_find_bottleneck,
    tool_send_message_user,
]

# Using LangChain Expression Language(LCEL)
# LCELを使用することで比較的簡単にシステムプロンプト（persona）を自分好みに変更することができます
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", persona),
        MessagesPlaceholder(variable_name="chat_history"),
        ("user", "{input}"),
        MessagesPlaceholder(variable_name="agent_scratchpad"),
    ]
)
# LangChainのToolからOpenAIのFunctionフォーマットに変換する
llm_with_tools = llm.bind(
    functions=[format_tool_to_openai_function(t) for t in tools]
)
# Memory用のプレースホルダーを用意
agent_kwargs = {
    "extra_prompt_messages": [
        MessagesPlaceholder(variable_name="chat_history")
    ],
}

# LCELでAgentを構築
agent = (
    {
        "input": lambda x: x["input"],
        "agent_scratchpad": lambda x: format_to_openai_function_messages(
            x["intermediate_steps"]
        ),
        "chat_history": lambda x: memory.load_memory_variables({})[
            "chat_history"
        ],
    }
    | prompt
    | llm_with_tools
    | OpenAIFunctionsAgentOutputParser()
)

agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    agent_kwargs=agent_kwargs,
    memory=memory,
    callbacks=[StreamingStdOutCallbackHandler()],
)
```

# チャットBot作成

`LLMで予定調整くん`という名前のBotを作成しました。
作成方法は、こちらをご参考ください。

https://qiita.com/mmclsntr/items/3da41a9d03d6dba16290

:::note warn
今回は、Bot Server として、AWS Lambdaを利用していますが、デフォルトメモリの128MBでは、Agentの処理が遅くなったため、10240MBなどに増やす必要がありました。
:::

# 予定調整

作成したBotを使用してみます。
登録済の予定は以下のようになっているとします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659208/358c829e-3325-c055-4a3a-6fa9803fcc97.png" alt="" width="400" height="100">

### シナリオ1: 空き時間を探して予定を追加する

このシナリオの流れは以下のようになります。

1. 12月25日の午前中の空き時間を検索する
2. 検索結果から時間を選択して全員に予定追加の依頼をする
3. 予定作成の許可を求められるので「許可」する

:::note info
カレンダーへの登録及び削除など行う際には、実行前にユーザーへ許可を求める設計にしています。
現状のAIはまだ完璧ではないため、システムへクリティカルな影響を与える可能性がある処理には、`Human-in-the-loop`を意図的に組み込む必要があると考えています。
:::

#### 予定作成関数 `create_event_with_attendees` の補足
複数人に同じ予定を入れる場合には、予定作成者以外を参加者リスト([Attendee](https://developers.worksmobile.com/jp/docs/calendar-default-event-user-create#Attendee))に入れるようにしました。
具体的には以下に示すように `users` のようにユーザーのリストを受け取るようにしました。こうすることにより、一括で参加者全てのカレンダーに同じ予定を追加することができます。
また、ユーザーへの確認も作成者に対して一度でよくなります。


```python
class EventUsersCreateInput(BaseModel):
    users: list[str] = Field(..., description="予定を作成したいユーザ名リスト。")
    title: str = Field(..., description="予定タイトル")
    description: str = Field("", description="予定の内容")
    start_time: str = Field(
        ...,
        description="予定開始時刻 **YYYYY-MM-DDThh:mm:ss** の形式に従う必要がある。",  # noqa
    )
    end_time: str = Field(
        ...,
        description="予定終了時刻 **YYYYY-MM-DDThh:mm:ss** の形式に従う必要がある。",  # noqa
    )
```

### シナリオ1実行動画

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659208/b9b37e2d-2546-55c6-d02e-644f98b8a64a.gif" alt="" width="500" height="200">


### 実行後のカレンダー

正しく予定追加されているようです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659208/71c9f023-7ffb-3629-0280-e2932626fbf2.png" alt="" width="400" height="100">


### シナリオ2: 空き時間が見つからない場合、参加者が最も参加可能な時間を見つけて予定を作成する

このシナリオの流れは以下のようになります。

1. 12月25日の午後(13:00-17:00)の空き時間を検索する
2. 見つからないので、ボトルネックになっている人を調べる
3. 1人を除き調整可能なので予定を作成し、調整してもらう旨のメッセージ送信の依頼をする

### シナリオ2実行動画

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659208/f4919da8-9a8a-2532-b4ab-a9a955b8dda7.gif" alt="" width="500" height="200">


### Botから送られてきたメッセージ

以下の画像は、調整依頼をされた木田さんのトーク画面です。
正しく送られているようです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659208/ce7302c6-8bc9-e75c-1573-ca0b0b8a8f14.png" alt="" width="300" height="50">

### 所感

AIの活用について、「クリスマス」や「午前中」など明確に日時を指定しなくても文脈を理解して処理できる点は非常に魅力的です。従来のチャットボットでこうした曖昧な表現に対応するためには、想定される応答を洗い出し、一つ一つルールを記述する必要があり非常に煩雑でしたが、LLMを使うことでとても柔軟なやり取りを簡単に実現することができます。

一方で、今回使用したOpenAIのモデルは、`gpt-3.5-turbo-0613`を使用していますが、実用面においては、少しレスポンスの遅延が気になりました。また、得られる結果も時折期待通りの結果にならない場合もあり、GPTのような基盤モデルの更なる精緻化やプロンプトの改良必要性を感じました。

実装面に関しては、空き時間取得、ボトルネックの調査もAIに任せることも可能でしたが、プログラムで実装した方が高速かつ安定した結果を得られるため、今回関数として実装しました。現在、ある程度のことはAIで実現できてしまいますが、AI利用には処理時間や料金面のコストもかかるため、コストに見合った設計をする必要があります。

# まとめ

LINE WORKS API x OpenAI APIを利用して、予定の調整Botを実装してみました。
LINE WORKS APIのようなAPIとFunction Callingは相性が良いと思います。
日常生活での面倒事をAIを活用して解決する参考になれば幸いです。