LLM(Gemini)だけで、SwitchBotを操作する。

## はじめに
Googleの生成AIであるGemini APIだけを活用して、SwitchBotに登録されたデバイスをPythonで操作する仕組みを作成しました。
この仕組みの目的は、ユーザーのあらゆる言葉での指示を受けて、家電を操作することです。LLMは[ChatGPTにSwitchBotを操作してもらう](https://note.com/potemayorca/n/n84bc5a0ff49f)にもある通り、高度な操作を行うことができると思っています。

自分なりに考えたロジックを今後見返す時のために作成しています。**実装方法を全て書いていないので注意してください。**

## 担当を分ける
プロジェクトでは、AIを「会話担当AI」と「SwitchBot取扱担当AI」の2つに分けています。会話担当AIは、ユーザーから指示を受け取り、その意図を解釈してSwitchBot取扱担当AIに指示を出す役割を持っています。一方で、SwitchBot取扱担当AIは、その指示を受けてSwitchBotのAPIを操作します。
この手法は段階的に処理を行うため、一種のCoT（Chain of Thought）手法とも言えるかもしれません。

この分業によるメリットは、会話担当AIはSwitchBotの詳細な仕様を知らずに、ただ「SwitchBot取扱担当AIに指示を出す」という単純な説明を把握するだけでよくなります。そのため、余分なトークン消費を避けることができたり、スケーラブルにすることができます。また、GeminiのAPIとルールベースを組み合わせることで、トークン消費を抑えつつ、安定性を高めながら効率化を図っています。

つまり、SwitchBot取扱担当AIは、マニュアル全体を毎回読むのではなく、必要な部分のみを適宜読み込むことにより、トークン消費の削減と迅速な対応を可能にしています。

簡単に言うと「担当を分ける」とは、会話担当AIがSwitchBot取扱担当AI(関数)を呼び出す仕組みです。

## どのようなロジックか
デバイスIDやマニュアルがそれぞれ決まっているので、ある程度ルールに則る必要があります。SwitchBotを操作する仲介にGeminiを組み込みます。

以下のような仕組みを取っています。記事に書いたのは赤い枠です(細くて見えづらいです)。
![gemini_switchbot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2464900/3ea872be-facf-0385-3350-75e22e559682.png)
赤い四角枠が「SwitchBot取扱担当AI」です。関数呼び出し用としての一つの関数になります。

そのため入力となる引数(オーダー)はGeminiが生成します。例えば、「リビングの電気をつける」や「寝室の気温を調べる」などがオーダーです。

デバイス情報リストは、ユーザーがあらかじめ登録したSwitchBotデバイスのリストです。

四角形は関数です。青色はGeminiが組み込まれています。紫色はルールベース(通常の関数)です。

唯一ある分岐はオーダーの趣旨が(Geminiによって判断されたモード)「ステータスの取得」か「デバイスの操作」です。

つまり、指示が操作または取得か判断し、選んだマニュアルに従って実行するのがSwitchBot取扱担当AIということです。
この流れであれば、GeminiはSwitchBot APIのパラメータを的確に指定してくれるはずです。

#### ロジックの補足
オーダーは毎回一つにされます。例えば、「リビングの気温を調べて、あと電気つけて」と言う場合、「リビングの気温を調べる」+「リビングの照明をつける」と二回に分けて関数呼び出しするようにします。
しかし問題ないです。会話担当AIが関数呼び出しする際、一つずつオーダーするように説明しておきます(OpenAPIのdescriptionに記載する)。
AIを使った関数は、生成AIではなく分類AIの方がいいと思いますが、ここではGeminiを使っています。Geminiだけで実現しているのが特徴です。

## 大まかな手順
手順はメインではありませんが、特徴的な部分だけ取り上げています。
GeminiやSwitchBot APIの使い方は省略します。
[Google AI  Studio](https://aistudio.google.com/app/prompts/new_chat)や[SwitchBot API-GitHub](https://github.com/OpenWonderLabs/SwitchBotAPI?tab=readme-ov-file)が参考です。

以下はフローチャートにおける(1)の関数です。
```python
def extract_device_name(order,device_list):
    # Create a list containing only deviceName
    device_name_list = [device["deviceName"] for device in device_list]

    input_content = f"""
    'deviceName' = Select the deviceName that「」is pointing to from [{str(device_name_list)}].
    'mode' = Select and input 'Get_status' or 'control_divice' that「」is pointing to.
    「{order}」
    """

    generation_config = {
        "response_schema": content.Schema(
            type = content.Type.OBJECT,
            properties = {
                "deviceName": content.Schema(type = content.Type.STRING),
                "mode": content.Schema(type = content.Type.STRING)}),
                "response_mime_type": "application/json",
    }

    response = switchbot_model.generate_content(contents=input_content, generation_config=generation_config)
    
    # 'Get_status' or 'control_divice' in mode
    response_dict = json.loads(response.text)

    for device in device_list: # searching
        if device["deviceName"] == response_dict["deviceName"]:
            break
            
    return device,response_dict["mode"]
```
device_listにはSwitchBot APIを使った [SwitchBot API Get device list](https://github.com/OpenWonderLabs/SwitchBotAPI?tab=readme-ov-file#get-device-list)にある方法で取得したリストを調整したものです。device_name_listは、そこから名前だけ入れます。
JSONモードのGeminiから辞書列を取得しdeviceNameの一致する「デバイス情報」+「ステータスの取得かデバイスの操作か」を得ます。

以下はフローチャートにおける(2)の関数です。
```python
def send_command_to_device(control_manual,order):

    input_content = f"""
        Extract or fill in the parameters from the user command '{order}' based on the following recommendation.
        Select the best fitting 'command', 'commandType', and 'commandParameter' from {json.dumps(control_manual, ensure_ascii=False)}.
    """

    generation_config = {
        "response_schema": content.Schema(
            type = content.Type.OBJECT,
            properties = {
                "deviceID": content.Schema(type = content.Type.STRING),
                "commandType": content.Schema(type = content.Type.STRING),
                "command": content.Schema(type = content.Type.STRING),
                "parameter": content.Schema(type = content.Type.STRING)
                }),
                "response_mime_type": "application/json"
        }
    
    response = switchbot_model.generate_content(contents=input_content, generation_config=generation_config)
    
    response_dict = json.loads(response.text)

    device_id = response_dict["deviceID"]
    command_type = response_dict["commandType"]
    command = response_dict["command"]
    command_parameter = response_dict["parameter"]

    response = control_divice(device_id,command,command_parameter,command_type)

    return response
```
この関数では、SwitchBot APIを操作するためのパラメータを指定するフェーズです。
ここでもGeminiをJsonモードにします。
control_diviceはSwitchBot APIを利用して実際に操作する関数です。(抜粋のため、定義はここにありません。)


## まとめ
「Geminiだけを使う」をテーマとして作成しました。生成AIとルールベースを交互に組み、担当者を分けることで、トークン消費を抑えて安定させることに成功しました。この方法であれば、SwitchBot APIが大幅な変更をしない限り、マニュアルを追加するだけで、新製品にも対応できると考えています。
エラーの対策があまりできていないため、改良していきたいです。また、フロントエンドも作成したいです。今後も様々な方法を試してみたいです。

記事に不備があれば、お知らせください。
この記事は説明をかなり省略しています。ソースコードやロジックについて詳細を知りたい方がいらっしゃれば、いつでもご連絡ください。

ご覧いただきありがとうございました！

## 参考
[Google AI  Studio](https://aistudio.google.com/app/prompts/new_chat)
[SwitchBot API-GitHub](https://github.com/OpenWonderLabs/SwitchBotAPI?tab=readme-ov-file)

※この記事は[ChatGPT](https://chatgpt.com)と共同で作成しました。
※ソースコードの作成に、一部[ChatGPT](https://chatgpt.com)を使用しています。