こんにちは。リンクアンドモチベーション・データユニットの みく / Ryo Koizumi (@slpwalks) です。
ChatGPT、盛り上がっていますよね。 弊社のモチベーションクラウドシリーズにおいても既に ChatGPT API を活用した機能を2つほどリリースさせて頂きました。 まだまだ続々開発中なので、期待して頂ければと思います。
こちらが弊社リードエンジニア梅原による、ChatGPT連携開発の裏側が見られる資料になります。
今日は先述の機能の裏側で使っている LangChain というライブラリの中から、 LangChain Tools という仕組みを調べる過程をご紹介した上で、OpenAI API を用いてGoogleカレンダーの予定を検索する簡単なエージェントを作成した話をします。
イントロダクション
- LangChain Tools の仕組みを用い、Googleカレンダーに自然言語でクエリを投げられる独自の Tool を作成しました。
- Agent に差し込むことで、自然言語を用いて自分のカレンダーの予定を教えてくれる bot を作成できるようにしました。
注意
- LangChain 0.0.135 時での情報です。LangChain は非常に開発が早いライブラリであり、今後インタフェースなどが大きく変わりうる可能性があるのでご注意ください。
こちらは宣伝です!
- 4/11(火)にChatGPT x CS をテーマにしたイベントに、弊社BizOps責任者の杉山と一緒に登壇します。
当日は杉山によるCSテックタッチ化の取り組みの説明のほか、ヘルススコアに応じて自動で改善アドバイスを行ってくれる 「ヘルススコアAIアドバイザー」 の参考デモンストレーションを行う予定です。
また 4/18 にイネーブリングチームの伊藤が「ChatGPT時代のエンジニア組織づくり」について登壇するイベントがあります。気になる方はぜひチェックして頂けるとありがたいです。
LangChain とは?
LangChain Tools とは?
- LLMを用いて作成されたエージェントが、外部の世界とやり取りをするための仕組み。
- …と言ってもなんのこっちゃ分からないと思うので、QuickStart を覗いてみます。
QuickStart
コード
from langchain.agents import load_tools from langchain.agents import initialize_agent from langchain.agents import AgentType from langchain.chat_models import ChatOpenAI from langchain.llms import OpenAI # First, let's load the language model we're going to use to control the agent. chat = ChatOpenAI(temperature=0) # Next, let's load some tools to use. Note that the `llm-math` tool uses an LLM, so we need to pass that in. llm = OpenAI(temperature=0) tools = load_tools(["serpapi", "llm-math"], llm=llm) # Finally, let's initialize an agent with the tools, the language model, and the type of agent we want to use. agent = initialize_agent(tools, chat, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True) # Now let's test it out! agent.run("Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?")
出力
> Entering new AgentExecutor chain... Thought: I need to use a search engine to find Olivia Wilde's boyfriend and a calculator to raise his age to the 0.23 power. Action: { "action": "Search", "action_input": "Olivia Wilde boyfriend" } Observation: Sudeikis and Wilde's relationship ended in November 2020. Wilde was publicly served with court documents regarding child custody while she was presenting Don't Worry Darling at CinemaCon 2022. In January 2021, Wilde began dating singer Harry Styles after meeting during the filming of Don't Worry Darling. Thought:I need to use a search engine to find Harry Styles' current age. Action: { "action": "Search", "action_input": "Harry Styles age" } Observation: 29 years Thought:Now I need to calculate 29 raised to the 0.23 power. Action: { "action": "Calculator", "action_input": "29^0.23" } Observation: Answer: 2.169459462491557 Thought:I now know the final answer. Final Answer: 2.169459462491557 > Finished chain. '2.169459462491557'
この例では、「オリヴィア・ワイルド(女優)の彼氏の年齢の 0.23 乗 は?」という質問を投げて、見事 「2.169459462491557」という答えを出しています。
以上のことを実現するには、下記の3つのステップが必要そうです。ログを見ると実際にそのような処理が行われていそうです。
1. オリヴィア・ワイルドの彼氏は誰かを答える。(A: ハリー・スタイルズ)
2. ハリー・スタイルズの年齢を答える。(A: 29歳)
3. 29の0.23乗を計算する。 (A: 2.169…)
オリヴィア・ワイルドの彼氏の情報にしろ、計算にしろ LLM には答えが無さそうであり、外部から情報を取ってくる必要がありそうです。
実装を見ると、”serpapi” と “llm-math” という2つのツールをロードしています。それぞれが “Search” と “Calculator” に対応していそうですが、インプットは文章しか渡していないので、中間のやり取りは全て内部で生成していることになります。
tools = load_tools(["serpapi", "llm-math"], llm=llm)
Tool とは何なのか?どうやって実行しているのか?
定義済みのツールの情報は ここ にあります。 この情報だけだと分からないので、例として Wikipedia Tool の実装を見てみます。
"""Tool for the Wikipedia API.""" from langchain.tools.base import BaseTool from langchain.utilities.wikipedia import WikipediaAPIWrapper class WikipediaQueryRun(BaseTool): """Tool that adds the capability to search using the Wikipedia API.""" name = "Wikipedia" description = ( "A wrapper around Wikipedia. " "Useful for when you need to answer general questions about " "people, places, companies, historical events, or other subjects. " "Input should be a search query." ) api_wrapper: WikipediaAPIWrapper def _run(self, query: str) -> str: """Use the Wikipedia tool.""" return self.api_wrapper.run(query) async def _arun(self, query: str) -> str: """Use the Wikipedia tool asynchronously.""" raise NotImplementedError("WikipediaQueryRun does not support async")
…シンプルすぎない?
WikipediaAPIWrapper についても Wikipedia の Pythonライブラリをラップしているだけなので、特別なことはしていなさそうです。
この時点で自分が分からないことは以下です。
- これを利用するにはツールチェインに
wikipedia
を挿入すればいいだけだが、複数のツールチェインから目的のツールをどのように選択するのかが分からない。 - ツールで定義されているのは
_run
と_arun
の2つの関数だけであり、引数は query のみである。action_input = query だとしたら、どうやって前段の文章から action_input を抽出しているのかが分からない。
Wikipedia だとクエリの幅が広いので、より狭いクエリが必要なはずの OpenWeatherMap Tool の実装を見てみます。
"""Tool for the OpenWeatherMap API.""" from langchain.tools.base import BaseTool from langchain.utilities import OpenWeatherMapAPIWrapper class OpenWeatherMapQueryRun(BaseTool): """Tool that adds the capability to query using the OpenWeatherMap API.""" api_wrapper: OpenWeatherMapAPIWrapper name = "OpenWeatherMap" description = ( "A wrapper around OpenWeatherMap API. " "Useful for fetching current weather information for a specified location. " "Input should be a location string (e.g. 'London,GB')." ) def __init__(self) -> None: self.api_wrapper = OpenWeatherMapAPIWrapper() return def _run(self, location: str) -> str: """Use the OpenWeatherMap tool.""" return self.api_wrapper.run(location) async def _arun(self, location: str) -> str: """Use the OpenWeatherMap tool asynchronously.""" raise NotImplementedError("OpenWeatherMapQueryRun does not support async")
???
run
の引数が location
になっていますが、基本的には同じ形式です。どうやってロケーションのみを受け付けているのでしょうか?
としばらく眺めていたら以下を発見しました。
description = ( "A wrapper around OpenWeatherMap API. " "Useful for fetching current weather information for a specified location. " "Input should be a location string (e.g. 'London,GB')." )
なるほど!!!!!(大興奮
つまりどういうことか?
LangChain Tool の description は ユーザーに向けての説明文ではなく、プロンプト表現である。
Useful for
以下でどんな時に用いるツールであるかを記述することで、Agentがツールを当てるための参考にしている。Input should be
以下でインプットの定義を指定することで、前段階から渡される出力を加工して、ツールに合うようにフィットさせる。
つまりこれは 自然言語を用いてインタフェースを記述している ということになります。
インプットの定義に従って受け取れるようにすれば、あとは通常のプログラミングと同じく引数に従ってAPIを投げるなどして、文章表現にして戻せばいいことになります。
自作Tool (GoogleCalendarTool) を作ってみる
ここまで分かったので、自然言語を用いて特定の日付の予定を Google カレンダーから検索する Tool を実装してみます。
インタフェース含めた実装は以下の通りにしました。
(クライアントは別にあります・ハンドリングなど省略しています)
from langchain.tools import BaseTool from google_calendar_client import GoogleCalendarClient from datetime import datetime, timedelta class GoogleCalendarTool(BaseTool): name = 'GoogleCalendarSearch' description = ( 'カレンダーの予定を探す場合に有用です。' 'インプットは JSON で、以下の2つのキーを持っています: "date" と "n"' 'date は、予定を検索する日付です。yyyy-mm-dd という形式か、「今日」、「明日」、「明後日」のいずれかです。' 'n は、予定を検索する数です。' ) credentials_file: str def _run(self, query: dict[str, str]) -> str: client = GoogleCalendarClient(self.credentials_file) date_query = query.get('date') if date_query == '今日': date = datetime.today() elif date_query == '明日': date = datetime.today() + timedelta(days=1) elif date_query == '明後日': date = datetime.today() + timedelta(days=2) elif date_query: date = datetime.strptime(query.get('date'), '%Y-%m-%d') else: return "I don't know what to do." n = query.get('n') response = client.get_events(date, max_results=int(n)) if len(response) == 0: return "予定はありませんでした。" else: events = [f"- 予定: {event['summary']}" for event in response] return f"{date_query} の予定は以下の通りです。\n\n" + "\n".join(events) async def _arun(self, query: str) -> str: raise NotImplementedError("does not support async")
あとは Agent にToolをロードさせて、自然文を入力してみます。
from langchain.agents import initialize_agent, AgentType from langchain.chat_models import ChatOpenAI from google_calendar_tool import GoogleCalendarTool from dotenv import load_dotenv import os load_dotenv() def main(): chat = ChatOpenAI(temperature=0) agent = initialize_agent( [GoogleCalendarTool(credentials_file=os.getenv( "GOOGLE_CREDENTIALS_FILE"))], chat, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True) result = agent.run("今日のカレンダーの予定を5つ教えて。無かったら無いと言って") print(result) if __name__ == "__main__": main()
> Entering new AgentExecutor chain... Question: 今日のカレンダーの予定を5つ教えて。無かったら無いと言って。 Thought: GoogleCalendarSearchツールを使って、今日の日付と予定数を指定して予定を検索します。 Action: { "action": "GoogleCalendarSearch", "action_input": { "date": "今日", "n": 5 } } Observation: 今日 の予定は以下の通りです。 - 予定: 歯医者にいく - 予定: プラスチック・古紙ごみを捨てる - 予定: ビン・カン・ペットボトルを捨てる Thought:予定が3つ見つかりました。これで回答を終了します。 Final Answer: 今日の予定は、歯医者にいく、プラスチック・古紙ごみを捨てる、ビン・カン・ペットボトルを捨てるの3つです。
出来た!!!!!!!!!!