🔗

Raspberry PiとChatGPTでつくるボイス・アシスタント・ロボット #11

2023/10/01に公開

thumbnail_4
ロボットのAgentsが複数のモジュール(ツール)を連鎖的に実行するイメージ

ツール(Tools)

LangChainには、汎用的に使用できる用途別のツールが用意されています。今回は代表的な汎用ツールとして、検索ツールと計算ツールを実装します。

https://python.langchain.com/docs/integrations/tools/ddg

DuckDuckGo検索ツールを使用することで、特定の質問を送信して情報を検索し、その結果を取得できます。このツールは、ますみ@エンジニアさんのブログ[1]で知りましたが、DuckDuckGo APIは無料で利用できるので、個人プロジェクトで使うには非常に便利です。

Math chain

https://python.langchain.com/docs/use_cases/more/code_writing/llm_math

計算ツールを使用することで、数学の問題を解くのに役立ち、ChatGPTの計算に関する弱点を克服できます。

検索ツール・計算ツールの実装

ex_bot_gpt_analyzer_7.py
# ライブラリのインポート ---(※1)
import openai, os, json, dotenv, datetime
# LagnChainのチャットモデルをOpenAiと指定してインポートする
from langchain.chat_models import ChatOpenAI
# LangChainのシステムメッセージを定義するライブラリをインポートする
from langchain.schema import SystemMessage
# エージェント カスタムプロンプト作成 のライブラリインポート
from langchain.agents import OpenAIFunctionsAgent
# エージェント ツールモジュール のライブラリインポート
from langchain.agents import Tool
from langchain import LLMMathChain
from langchain.tools import DuckDuckGoSearchRun
# エージェントのランタイムを作成するライブラリインポート
from langchain.agents import AgentExecutor
# プロンプトに記憶用の場所を追加するライブラリ
from langchain.prompts import MessagesPlaceholder
# メモリー 全ての会話履歴を保持する ライブラリインポート
from langchain.memory import ConversationBufferMemory

# .envファイルから環境変数をロード ---(※2)
dotenv.load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")

# エージェントの制御に使用する言語モデルをロード ---(※3)
llm = ChatOpenAI(model_name="gpt-3.5-turbo-0613", temperature=0)

# 計算ツールを定義 ---(※4)
llm_math_chain = LLMMathChain.from_llm(llm=llm, verbose=True)

# 検索ツールを定義 ---(※5)
search = DuckDuckGoSearchRun()

# 利用可能なツールを指定 ---(※6)
tools = [
    Tool(
        name="Calculator",
        func=llm_math_chain.run,
        description="数学に関する質問に答える必要がある場合に使用します"
    ),
    Tool(
        name="duckduckgo-search",
        func=search.run,
        description="""
            ###目的###
            必要な情報を得るためウェブ上の最新情報を検索します
            
            ###回答例###
            Q: 東京の今日の天気予報を教えて
            A: 東京都の本日の天気予報は晴れのち曇り最高気温32度最低気温25度 今日も暑くなるでしょう

            ###制限###
            回答は140文字以内でおこなってください
            """,
    )
]

# プロンプトを作成 ヘルパー関数を使用して、OpenAIFunctionsAgent.create_promptプロンプトを自動的に作成 ---(※7)
system_message = SystemMessage(content="""
                            あなたは垂直方向と水平方向に移動するカメラを搭載した音声チャットロボットです。
                            名前は「ゆっくり霊夢」です。
                            """)
                        
# プロンプトに記憶用の場所を追加 キーを使用してメッセージのプレースホルダーを追加 ---(※8)
MEMORY_KEY = "chat_history"
prompt = OpenAIFunctionsAgent.create_prompt(
    system_message=system_message,
    extra_prompt_messages=[MessagesPlaceholder(variable_name=MEMORY_KEY)]
) 

# メモリオブジェクトを作成  ---(※9)
memory = ConversationBufferMemory(memory_key=MEMORY_KEY, return_messages=True)

def chat_with_agent(text):
    result = None  # 初期化
    try:
        # これらの部分を組み合わせ、エージェントを作成 ---(※10)
        agent = OpenAIFunctionsAgent(llm=llm, tools=tools, prompt=prompt)

        # エージェントのランタイムである AgentExecutor を作成 ---(※11)
        agent_executor = AgentExecutor(agent=agent, tools=tools, memory=memory, verbose=True)

        # ランタイムを実行 ---(※12)
        result = agent_executor.run(text)
        #print(str(result))

        return str(result)

    except Exception as e: # エラーハンドリング ---(※13)
        print(f"SYSTEM: エラーが発生しました: {e}")
        return None

if __name__ == "__main__":
    print("🖥️. SYSTEM: チャットを開始します。終了するには '/exit' を入力してください。")

    # ターミナルで連続して対話するループ ---(※14)
    while True:
        user_input = input("😀 USER: ")
        if user_input == "/exit":
            print("🖥️. SYSTEM: チャットを終了します。")
            break

        # ChatGPTによる応答を取得 ---(※15)
        assistant_reply = chat_with_agent(user_input)

        # ChatGPTの応答を表示
        print("🤖 GPT: " + assistant_reply)

(※1)で必要なライブラリをインポートし、(※2)で.envファイルからChatGPTのAPIキーををロードします。ここでは読み込みにdotenvモジュールを使用します。

(※3)でエイージェントの制御に使用するモデルをロードします。モデルはgpt-3.5-turbo-0613、テキスト生成機能はChatOpenAIで指定します。

(※4)(※5)でドキュメントに沿って計算ツール、検索ツールを定義します。

(※6)でパラメータを定義します。呼び出すツールはtoolsで定義します。複数形で表されているように、このパラメータは複数の要素を入れた配列で定義され、エージェントはこの要素を選択します。

(※7)〜(※15)は前章で実装したカスタムツールと同じ実装でエージェントは機能します。実行結果は以下のようになります。

terminal
🖥️. SYSTEM: チャットを開始します。終了するには '/exit' を入力してください。
😀 USER: 100掛ける25足す150掛ける10は

> Entering new AgentExecutor chain...
Invoking: `Calculator` with `100 * 25 + 150 * 10`

> Entering new LLMMathChain chain...
100 * 25 + 150 * 10 
...numexpr.evaluate("100 * 25 + 150 * 10")...

Answer: 4000
> Finished chain.
Answer: 4000100掛ける25は2500、150掛ける10は1500なので、2500足す1500は4000です。

> Finished chain.
🤖 GPT: 100掛ける25は2500、150掛ける10は1500なので、2500足す1500は4000です。
terminal
😀 USER: 明日の東京の天気予報を 検索してください

> Entering new AgentExecutor chain...
Invoking: `duckduckgo-search` with `東京の明日の天気予報`

東京都の天気 07() 08() 09() 10() 11() 12() 13() 14() 15() 16() 17() 07日10:00発表 09月07日 () 千代田区 33/24 60% 新宿区 33/24 60% 世田谷区 32/25 80% 三鷹市 32/24 60% 八王子市 32/23 80% 青梅市 32/22 70% 大島町 29/24... 東京(東京)の天気予報。 今日・明日の天気と風と波、明日までの6時間ごとの降水確率と最高・最低気温を見られます。 東京(東京)の天気 - Yahoo!天気・災害 東京都の天気予報。今日の天気、明日の天気、気温、降水確率を地図上に表示。近隣地域の天気も一目でわかります。 世田谷区の今日明日の天気 - 日本気象協会 tenki.jp 世田谷区の今日明日の天気、気温、降水確率に加え、台風情報、警報注意報、観測ランキング、紫外線指数等を掲載。 気象予報士が日々更新する「日直予報士」や季節を楽しむコラム「tenki.jpサプリ」などもチェックできます。... 毎時更新【ウェザーニュース】東京の1時間毎・今日明日・週間(10日間)の天気予報、いまの空模様。世界最大の民間気象情報会社ウェザーニューズの日本を網羅する観測ネットワークと独自の予測モデル、ai分析で一番当たる予報をお届け。東京の明日の天気予報は、晴れのち曇りで最高気温は33度、最低気温は24度です。降水確率は60%です。明日も暑くなるでしょう。

> Finished chain.
🤖 GPT: 東京の明日の天気予報は、晴れのち曇りで最高気温は33度、最低気温は24度です。降水確率は60%です。明日も暑くなるでしょう。
😀 USER: /exit
🖥️. SYSTEM: チャットを終了します。

エージェントによる各ツールの実行

いよいよ、汎用ツールとあわせて、前章で作成したカスタムツールをエージェントが実行出来るように実装します。

chart6

2章で示したチャート図の赤い枠で囲んだ部分であり、サンプルプログラムはbot_gpt_analyzer.py
になります。このファイルのツールを一部抜粋したもので解説します。

ex_bot_gpt_analyzer_8.py
# ライブラリのインポート ---(※1)
import openai, os, json, dotenv, datetime
# LagnChainのチャットモデルをOpenAiと指定してインポートする
from langchain.chat_models import ChatOpenAI
# LangChainのシステムメッセージを定義するライブラリをインポートする
from langchain.schema import SystemMessage
# エージェント カスタムプロンプト作成 のライブラリインポート
from langchain.agents import OpenAIFunctionsAgent
# エージェント ツールモジュール のライブラリインポート
from langchain.agents import tool
from langchain.agents import Tool
from langchain import LLMMathChain
from langchain.tools import DuckDuckGoSearchRun
# エージェントのランタイムを作成するライブラリインポート
from langchain.agents import AgentExecutor
# プロンプトに記憶用の場所を追加するライブラリ
from langchain.prompts import MessagesPlaceholder
# メモリー 全ての会話履歴を保持する ライブラリインポート
from langchain.memory import ConversationBufferMemory

# .envファイルから環境変数をロード ---(※2)
dotenv.load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")

# エージェントの制御に使用する言語モデルをロード ---(※3)
llm = ChatOpenAI(model_name="gpt-3.5-turbo-0613", temperature=0)

# 現在時刻を取得するツールを定義 ---(※4)
@tool
def get_date_time() -> json:
    """datetime関数をつかい、「現在時刻」「今日の日付」を返します"""
    day_now = datetime.datetime.today().strftime("%-Y年%-m月%-d日")
    time_now = datetime.datetime.now().strftime("%-H時%-M分")
    date_time_data = {
        "day_now": day_now,
        "time_now": time_now,
    }
    return json.dumps(date_time_data)

# pan、tilt角度を生成するツールを定義 ---(※5)
@tool
def turn_pan_tilt(pan, tilt):
    """
        ###目的###
        テキストで方向を指示された場合
        パラメータ "pan"(水平)、"tilt"(垂直)を数値化し、その値を返します

        ###数値化するパラメータ###
        - "pan": -90 < pan < 90
        - "tilt": -90 < tilt < 90

        ###出力の例###
        Q: "右を向いて"
        A: "pan": -90,"tilt": 0

        Q: "左を向いて"
        A: "pan": 90,"tilt": 0
        
        Q: "上を向いて"
        A: "pan": 0, "tilt": -90

        Q: "下を向いて"
        A: "pan": 0, "tilt": 90

        Q: "右上を向いて"
        A: "pan": -90, "tilt": -90
        
        Q: 左下を向いて
        A:
    """
    turn_degree = {
        "pan": pan,
        "tilt": tilt,
    }

    return  json.dumps(turn_degree)

# 計算ツールを定義 ---(※4)
llm_math_chain = LLMMathChain.from_llm(llm=llm, verbose=True)

# 検索ツールを定義 ---(※5)
search = DuckDuckGoSearchRun()

# 利用可能なツールを指定 ---(※6)
tools = [
    Tool(
        name="Calculator",
        func=llm_math_chain.run,
        description="数学に関する質問に答える必要がある場合に使用します"
    ),
    Tool(
        name="duckduckgo-search",
        func=search.run,
        description="""
            ###目的###
            必要な情報を得るためウェブ上の最新情報を検索します
            
            ###回答例###
            Q: 東京の今日の天気予報を教えて
            A: 東京都の本日の天気予報は晴れのち曇り最高気温32度最低気温25度 今日も暑くなるでしょう

            ###制限###
            回答は140文字以内でおこなってください
            """,
    ),
    get_date_time, 
    turn_pan_tilt
]

# プロンプトを作成 ヘルパー関数を使用して、OpenAIFunctionsAgent.create_promptプロンプトを自動的に作成 ---(※7)
system_message = SystemMessage(content="""
                            あなたは垂直方向と水平方向に移動するカメラを搭載した音声チャットロボットです。
                            名前は「ゆっくり霊夢」です。
                            """)
                        
# プロンプトに記憶用の場所を追加 キーを使用してメッセージのプレースホルダーを追加 ---(※8)
MEMORY_KEY = "chat_history"
prompt = OpenAIFunctionsAgent.create_prompt(
    system_message=system_message,
    extra_prompt_messages=[MessagesPlaceholder(variable_name=MEMORY_KEY)]
) 

# メモリオブジェクトを作成  ---(※9)
memory = ConversationBufferMemory(memory_key=MEMORY_KEY, return_messages=True)

def chat_with_agent(text):
    result = None  # 初期化
    try:
        # これらの部分を組み合わせ、エージェントを作成 ---(※10)
        agent = OpenAIFunctionsAgent(llm=llm, tools=tools, prompt=prompt)

        # エージェントのランタイムである AgentExecutor を作成 ---(※11)
        agent_executor = AgentExecutor(agent=agent, tools=tools, memory=memory, verbose=True)

        # ランタイムを実行 ---(※12)
        result = agent_executor.run(text)
        #print(str(result))

        return str(result)

    except Exception as e: # エラーハンドリング ---(※13)
        print(f"SYSTEM: エラーが発生しました: {e}")
        return None

if __name__ == "__main__":
    print("🖥️. SYSTEM: チャットを開始します。終了するには '/exit' を入力してください。")

    # ターミナルで連続して対話するループ ---(※14)
    while True:
        user_input = input("😀 USER: ")
        if user_input == "/exit":
            print("🖥️. SYSTEM: チャットを終了します。")
            break

        # ChatGPTによる応答を取得 ---(※15)
        assistant_reply = chat_with_agent(user_input)

        # ChatGPTの応答を表示
        print("🤖 GPT: " + assistant_reply)

前章、前節で作成したツールを両方エージェントツールとして指定します。ポイントとなるのは、作成したツールを配列に格納することだけです。

(※6)でパラメータtoolsの設定を行います。その際、各ツールの関数名を要素として記述します。カスタムツールと汎用ツールで記述が異なるので、その部分だけ気をつけてください。

(※10)でOpenAIFunctionsAgentを定義する際、パラメータtoolsに作成した配列を指定します。

エージェントは、ユーザーの入力に応じて異なるツールを呼び出すことができます。汎用ツールと自作ツールの併用が行われるか確認してみます。

terminal
🖥️. SYSTEM: チャットを開始します。終了するには '/exit' を入力してください。
😀 USER: 100掛ける25足す150掛ける10は

> Entering new AgentExecutor chain...
Invoking: `Calculator` with `100 * 25 + 150 * 10`

> Entering new LLMMathChain chain...
100 * 25 + 150 * 10`

...numexpr.evaluate("100 * 25 + 150 * 10")...

Answer: 4000
> Finished chain.
Answer: 4000100掛ける25は2500、150掛ける10は1500です。それらを足すと4000になります。

> Finished chain.
🤖 GPT: 100掛ける25は2500、150掛ける10は1500です。それらを足すと4000になります。
😀 USER: 少し上を向いてください

> Entering new AgentExecutor chain...
Invoking: `turn_pan_tilt` with `{'pan': 0, 'tilt': -30}`

{"pan": 0, "tilt": -30}了解しました。少し上を向くようにカメラの角度を調整します。

> Finished chain.
🤖 GPT: 了解しました。少し上を向くようにカメラの角度を調整します。
😀 USER: /exit
🖥️. SYSTEM: チャットを終了します。

汎用ツールToolと独自で定義した@toolを併用しているのが確認できます。

エージェントのチェーンアクション

LangChainはその名が示すとおり、各モジュールを連鎖的(Chain)に実行することが出来ます。エージェントは複数の指示を解釈し、連鎖的に実行することも出来ます。

1章のサンプルを再度確認してみましょう。

chain_1

terminal
😀 USER:  ユーザーを検索してその関心について検索してください

ユーザーの音声入力をエージェントに渡し、ChatGPTへ送信します。ここでは、複数の指示を送っています。

chain_2

termnal
> Entering new AgentExecutor chain...

Invoking: `get_user_info` with `{}`

まずひとつめのモジュールを選択しました。カメラを使った顔認証をおこない、その結果からユーザーデータを参照します。

chain3

terminal
{"recognized_id": "vincent", "user_name": "\u30d3\u30f3\u30bb\u30f3\u30c8", "user_category": "man", "user_interested": "\u30c4\u30a4\u30b9\u30c8(\u30c0\u30f3\u30b9)"}

Invoking: `duckduckgo-search` with `ツイスト ダンス`
responded: {content}

> Finished chain.
🤖 GPT:  ツイストは1960年代前半に世界的な流行を見た社交ダンスで、特に腰をひねる動きが特徴です。初心者でも簡単にできる練習方法や基本ステップのやり方などがあります。YouTubeなどの動画サイトでもツイストのダンスレッスンが公開されていますので、興味がある方はぜひ挑戦してみてください!

ユーザーIDから、事前に登録されたユーザーの関心を取得しました。チェーンアクションにより、モジュールの実行結果(ここでは「ツイスト ダンス」)を次のモジュールへ渡しています。

ここではDuckDuckGO search APIを使い「ツイスト ダンス」を検索しています。

chain4

複数の指示をエージェントが自律的に解釈し、モジュールを連鎖的に実行しています。

脚注
  1. 検索用APIを使わずにウェブページをChatGPTに学習させる方法【Python / LangChain / FAQ】/ Zenn ↩︎

  2. Conversational / 🦜️🔗 LangChain ↩︎

Discussion