「LangChain、Google Maps API、Gradioを使用してスマートな旅行スケジュール案内ツールを作る(パート1)」

「グラディオを用いてスマートな旅行スケジュール案内ツールを作る方法(パート1)」

次のロードトリップのインスピレーションになる可能性のあるアプリケーションの構築方法を学びましょう

この記事は、OpenAIとGoogleのAPIを使用し、gradioで生成されたシンプルなUIで表示される旅行行程提案アプリケーションを構築するための3部作の第1部です。このパートでは、このプロジェクトのためのプロンプトエンジニアリングについて説明します。ただのコードを見たいだけですか?それならこちらを見つけてください

1. 動機

ChatGPTのリリース以来、2022年末から、大規模言語モデル(LLM)とそれらの消費者向け製品への応用、例えばチャットボットや検索エンジンへの応用における興味が急増しています。その後1年も経たないうちに、Hugging Faceのようなモデルハブ、Laminiのようなモデルホスティングサービス、OpenAIやPaLMのような有料のAPIなど、様々なオープンソースのLLMにアクセスできるようになりました。この分野がいかに速く進化しているか、数週間ごとに新しいツールと開発パラダイムが現れていることを見るのは、興奮すると同時に多少圧倒されるものです。

ここでは、旅行計画の支援に役立つ便利なアプリケーションを構築するためのツールの一部を取り上げます。休暇の計画を立てる際には、既に訪れたことのある場所からの提案を受けることがよくあります。それらの提案が地図上に表示されると尚のこと良いです。このアドバイスがない場合、私は行きたい場所の周辺をGoogleマップで閲覧し、興味深そうな場所を何軒か無秩序に選んでいます。このプロセスは楽しいかもしれませんが、効率が悪く、何かを見逃す可能性もあります。一部の高レベルの優先事項を指定するだけで、多くの提案を行えるツールがあれば素晴らしいと思いませんか?

それが私たちが構築しようとしているものです:高レベルの優先事項を指定すると、旅行行程提案を行うシステム。例えば、「サンフランシスコを3日間探索し、美術館が大好きです」といったものです。Google検索の生成型AI機能やChatGPTは、すでにこのようなクエリに対して創造的な結果を出力することができますが、私たちはさらに進んで、旅行時間と見やすい地図を提供する実際の行程を作成したいと考えています。

これが私たちが構築するものです:LLMによって提供されるルートとウェイポイントを示す基本的な地図で旅行提案を生成するシステム

このゴールは、実際にアプリケーションを展開することよりも、このようなサービスを構築するために必要なツールに慣れることですが、その過程でプロンプトエンジニアリング、LangChainを使用したLLMのオーケストレーション、Google Maps APIを使用したルート抽出、leafmapgradioを使用した結果の表示についても少し学びます。これらのツールを使用すると、このようなシステムのPOCを非常に迅速に構築することができますが、評価やエッジケースの管理の真の課題は常に残ります。私たちが構築するツールは完全ではなく、さらなる開発に興味がある方がいれば、その協力は素晴らしいことです。

2. プロンプティング戦略

このプロジェクトでは、OpenAIとGoogle PaLM APIを使用します。APIキーはここここでアカウントを作成して取得することができます。執筆時点では、Google APIは一部のユーザーにしか利用できず、待機リストがあるということですが、数日でアクセスできるようになるはずです。

dotenvを使用すると、APIキーを開発環境にコピー&ペーストする手間を省くことができます。次の行が含まれた.envファイルを作成した後に

OPENAI_API_KEY = {あなたのopen ai key}GOOGLE_PALM_API_KEY = {あなたのgoogle palm api key}

この関数を使用して、LangChainなどでダウンストリームで使用する変数をロードすることができます

from dotenv import load_dotenv
from pathlib import Path

def load_secrets():
    load_dotenv()
    env_path = Path(".") / ".env"
    load_dotenv(dotenv_path=env_path)
    open_ai_key = os.getenv("OPENAI_API_KEY")
    google_palm_key = os.getenv("GOOGLE_PALM_API_KEY")
    return {
        "OPENAI_API_KEY": open_ai_key,
        "GOOGLE_PALM_API_KEY": google_palm_key,
    }

さて、旅行代理店サービス向けのプロンプトをどのように設計すれば良いでしょうか?ユーザーは自由にテキストを入力できるため、まずはクエリが有効かどうかを判断できるようにしたいと思います。有害なコンテンツを含むクエリは必ずフラグ付けしたいので、悪意を持った行程表のリクエストなどを含むものです。

また、旅行に関係のない質問をフィルタリングしたいです。確かにLLMはそのような質問に対して回答を提供できるかもしれませんが、それらはこのプロジェクトの範囲外です。最後に、ニューヨークから東京までの3日間のロードトリップなどの非合理なリクエストも特定したいと考えています。このような非合理なリクエストがある場合、モデルがなぜ非合理であるかを説明し、改善するための修正を提案してくれると嬉しいです。

リクエストが検証されたら、マッピングや案内などのAPI(たとえばGoogle Maps)に送信できるように、提案された行程表を作成できます。

行程表は人が読める形式で、単独の提案として有用であるための十分な詳細を含んでいる必要があります。ChatGPTなどの大規模な命令チューニングLLMは、そのような応答の提供において非常に優れているようですが、ウェイポイントのアドレスが一貫した方法で抽出されることを確認する必要があります。

したがって、ここで3つの明確なステージがあります:

  1. クエリを検証する
  2. 行程表を作成する
  3. Google Maps APIで理解できる形式でウェイポイントを抽出する

これらを一度に行うプロンプトを設計することも可能ですが、デバッグのためにそれらを3つのLLMコールに分割します。

幸いにも、LangChainのPydanticOutputParserはここで本当に役立ちます。このモジュールは、応答を出力スキーマに従う形式で整形するようLLMに促すための事前作成されたプロンプトを提供します。

3. 検証プロンプト

まず、異なるバージョンを含めるのが簡単になるように、テンプレートクラスでラップできる検証プロンプトを見てみましょう。

from langchain.prompts.chat import (
    ChatPromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
)
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

class Validation(BaseModel):
    plan_is_valid: str = Field(
        description="This field is 'yes' if the plan is feasible, 'no' otherwise"
    )
    updated_request: str = Field(description="Your update to the plan")

class ValidationTemplate(object):
    def __init__(self):
        self.system_template = """      
        You are a travel agent who helps users make exciting travel plans.      
        The user's request will be denoted by four hashtags. Determine if the user's      
        request is reasonable and achievable within the constraints they set.      
        A valid request should contain the following:      
        - A start and end location      
        - A trip duration that is reasonable given the start and end location      
        - Some other details, like the user's interests and/or preferred mode of transport      
        Any request that contains potentially harmful activities is not valid, regardless of what      
        other details are provided.      
        If the request is not valid, set      
        plan_is_valid = 0 and use your travel expertise to update the request to make it valid,      
        keeping your revised request shorter than 100 words.      
        If the request seems reasonable, then set plan_is_valid = 1 and      
        don't revise the request.      
        {format_instructions}    """
        self.human_template = """      
        ####{query}####    """
        self.parser = PydanticOutputParser(pydantic_object=Validation)
        self.system_message_prompt = SystemMessagePromptTemplate.from_template(
            self.system_template,
            partial_variables={
                "format_instructions": self.parser.get_format_instructions()
            },
        )
        self.human_message_prompt = HumanMessagePromptTemplate.from_template(
            self.human_template, input_variables=["query"]
        )
        self.chat_prompt = ChatPromptTemplate.from_messages(
            [self.system_message_prompt, self.human_message_prompt]
        )

私たちのValidationクラスには、クエリの出力スキーマ定義が含まれています。これは、plan_is_validupdated_requestの2つのキーを持つJSONオブジェクトになります。InsideValidationTemplateでは、LangChainの便利なテンプレートクラスを使用してプロンプトを構築し、PydanicOutputParserと呼ばれるパーサーオブジェクトも作成します。これにより、PydanticコードをValidation内の指示のセットに変換し、クエリと一緒にLLMに渡すことができます。システムテンプレートには、これらの形式指示への参照を含めることができます。APIが呼び出されるたびに、system_message_prompthuman_message_promptの両方をLLMに送信したいので、それらをchat_promptにまとめます。

これは実際にはチャットボットアプリケーションではないので(もちろん、チャットボットにすることもできます!)、システムとユーザーのテンプレートを同じ文字列に入れて同じ応答を得ることができます。

次に、上記で定義したテンプレートを使用してLLM APIを呼び出すAgentクラスを作成できます。ここではChatOpenAIを使用していますが、お好みでGooglePalmに置き換えることもできます。

注意点として、ここでは1回のLLM呼び出ししか行っていませんが、LangchainのLLMChainSequentialChainも使用しています。これはおそらくオーバーキルですが、将来的に他の呼び出し(例えば、バリデーションチェーンの実行前に OpenAIモデレーションAPIへの呼び出し)を追加する場合に役立つかもしれません。

import openaiimport loggingimport time# for Palmfrom langchain.llms import GooglePalm# for OpenAIfrom langchain.chat_models import ChatOpenAIfrom langchain.chains import LLMChain, SequentialChainlogging.basicConfig(level=logging.INFO)class Agent(object):    def __init__(        self,        open_ai_api_key,        model="gpt-3.5-turbo",        temperature=0,        debug=True,    ):        self.logger = logging.getLogger(__name__)        self.logger.setLevel(logging.INFO)        self._openai_key = open_ai_api_key        self.chat_model = ChatOpenAI(model=model, temperature=temperature, openai_api_key=self._openai_key)        self.validation_prompt = ValidationTemplate()        self.validation_chain = self._set_up_validation_chain(debug)    def _set_up_validation_chain(self, debug=True):              # make validation agent chain        validation_agent = LLMChain(            llm=self.chat_model,            prompt=self.validation_prompt.chat_prompt,            output_parser=self.validation_prompt.parser,            output_key="validation_output",            verbose=debug,        )                # add to sequential chain         overall_chain = SequentialChain(            chains=[validation_agent],            input_variables=["query", "format_instructions"],            output_variables=["validation_output"],            verbose=debug,        )        return overall_chain    def validate_travel(self, query):        self.logger.info("Validating query")        t1 = time.time()        self.logger.info(            "Calling validation (model is {}) on user input".format(                self.chat_model.model_name            )        )        validation_result = self.validation_chain(            {                "query": query,                "format_instructions": self.validation_prompt.parser.get_format_instructions(),            }        )        validation_test = validation_result["validation_output"].dict()        t2 = time.time()        self.logger.info("Time to validate request: {}".format(round(t2 - t1, 2)))        return validation_test

以下のコードを実行して例を試すことができます。debug=Trueを設定すると、LangChainのデバッグモードが有効になり、クエリテキストがLLM呼び出しの各LangChainクラスを通過する際に進行状況が出力されます。

secrets = load_secets()travel_agent = Agent(open_ai_api_key=secrets[OPENAI_API_KEY],debug=True)query = """        I want to do a 5 day roadtrip from Cape Town to Pretoria in South Africa.        I want to visit remote locations with mountain views        """travel_agent.validate_travel(query)

このクエリは合理的なので、次のような結果が得られます

INFO:__main__:Validating queryINFO:__main__:Calling validation (model is gpt-3.5-turbo) on user inputINFO:__main__:Time to validate request: 1.08{'plan_is_valid': 'yes', 'updated_request': ''}

今回は、クエリをより合理的でないものに変更してテストしてみます。以下のようなものです。

query = """        私は南アフリカのケープタウンからプレトリアまで歩きたいです。        山岳景観のある遠い場所を訪れたいです。        """

応答時間が長くなります。ChatGPTはクエリが無効であることの説明を提供しようとしており、そのためにより多くのトークンを生成しています。

INFO:__main__:クエリを検証INFO:__main__:ユーザー入力の検証を呼び出す(モデルはgpt-3.5-turbo)INFO:__main__:リクエストを検証するための時間: 4.12{'plan_is_valid': 'no', 'updated_request': 'ケープタウンからプレトリアまで歩くことはできない...' a

4.旅程のプロンプト

クエリが有効であれば、次のステージである旅程プロンプトに移動することができます。ここでは、モデルに詳細な旅行プランを提案してもらいたいので、ウェイポイントの住所とそれぞれの場所でのアドバイスが入った箇条書きリストの形式で提供してもらいたいです。これがプロジェクトの主な「生成」の部分であり、ここでは良い結果を得るためのクエリの設計方法はさまざまあります。私たちのItineraryTemplateは以下のようになります。

class ItineraryTemplate(object):    def __init__(self):        self.system_template = """      ユーザーのリクエストに基づいて、興奮を持って旅行プランを提案する旅行代理店です。      ユーザーのリクエストは4つのハッシュタグで表されます。ユーザーのリクエストを、訪れるべき場所とそれぞれの場所で行うべきことを記述した詳細な旅程に変換してください。      各場所の具体的な住所を含めるようにしてください。      ユーザーの好みや時間枠を考慮し、制約を考慮に入れた楽しく実現可能な旅程を提供してください。      開始地点と終了地点が明示されていない場合は、適切と思われる開始地点と終了地点を選んで具体的な住所を提供してください。      出力はリストである必要があります。    """        self.human_template = """      ####{query}####    """        self.system_message_prompt = SystemMessagePromptTemplate.from_template(            self.system_template,        )        self.human_message_prompt = HumanMessagePromptTemplate.from_template(            self.human_template, input_variables=["query"]        )        self.chat_prompt = ChatPromptTemplate.from_messages(            [self.system_message_prompt, self.human_message_prompt]        )

ここではPydanticパーサーは必要ありません。出力はJSONオブジェクトではなく、文字列である必要があるためです。

これを使用するには、Agentクラスに新しいLLMChainを追加することができます。以下のような形になります。

        travel_agent = LLMChain(            llm=self.chat_model,            prompt=self.itinerary_prompt.chat_prompt,            verbose=debug,            output_key="agent_suggestion",        )

ここではchat_modelをインスタンス化する際にmax_tokens引数を設定していません。これにより、モデルは出力の長さを自ら決定することができます。特にGPT4の場合、これにより応答時間がかなり長くなることがあります(一部の場合では30秒以上)。興味深いことに、PaLMの応答時間はかなり短いです。

5.ウェイポイント抽出のプロンプト

旅程のプロンプトを使用すると、素敵なウェイポイントのリストが得られるかもしれません。以下のようなものです。

- Day 1:  - バークレー(カリフォルニア州)で出発  - レッドウッド国立公園(カリフォルニア州、セカンドストリート1111、クレセントシティ)へのドライブ  - 美しいレッドウッドの森を探索し、自然を楽しむ  - ユーレカ(カリフォルニア州、セカンドストリート531、ユーレカ)へのドライブ  - 地元の料理を味わい、魅力的な街を探索  - ユーレカ(カリフォルニア州)で宿泊- Day 2:  - ユーレカ(カリフォルニア州)で出発  - クレーターレイク国立公園(オレゴン州、クレーターレイク国立公園、97604)で驚くべき青い湖を見て、景色の良いトレイルをハイキング  - ベンド(オレゴン州、Bend、97701)へのドライブ  - 地元の食文化を楽しみ、活気溢れる街を探索  - ベンド(オレゴン州)で宿泊- Day 3:  - ベンド(オレゴン州)で出発  - マウントレーニア国立公園(ワシントン州、星の238番地E、アシュフォード)で息をのむような山の景色を楽しみ、トレイルをハイキング  - タコマ(ワシントン州、タコマ)へのドライブ  - 美味しい食べ物を試し、街の観光名所を探索  - タコマ(ワシントン州)で宿泊- Day 4:  - タコマ(ワシントン州)で出発  - オリンピック国立公園(ワシントン州、マウントアンジェルズロード3002、ポートアンジェルス)で多様な生態系を探索し、自然の美しさを堪能  - シアトル(ワシントン州、シアトル)へのドライブ  - 活気ある食文化を体験し、人気のある観光名所を訪れる  - シアトル(ワシントン州)で宿泊- Day 5:  - シアトル(ワシントン州)で出発  - 街の観光名所をもっと探索し、地元の料理を楽しむ  - 旅行の終了地点: シアトル(ワシントン州)今度は、次のステップに進むためにウェイポイントのアドレスを抽出する必要があります。それは地図上にプロットし、Google Mapsの方向APIを呼び出してそれらの間の経路を取得することになります。

これを行うために、別のLLMコールを行い、PydanicOutputParserを再度使用して出力が正しくフォーマットされていることを確認します。ここでのフォーマットを理解するために、このプロジェクトの次のステージでやりたいこと(パート2で説明されている)を簡単に考えてみると役立ちます。具体的には次のようなGoogle Maps Python APIを呼び出すことになります。

import googlemapsgmaps = googlemaps.Client(key=google_maps_api_key)directions_result = gmaps.directions(            start,            end,            waypoints=waypoints,            mode=transit_type,            units="metric",            optimize_waypoints=True,            traffic_model="best_guess",            departure_time=start_time,)

ここで、startとendは文字列形式のアドレスであり、waypointsはその間に訪れるアドレスのリストです。

ウェイポイント抽出のためのリクエストスキーマは次のようになります。

class Trip(BaseModel):    start: str = Field(description="出発地")    end: str = Field(description="到着地")    waypoints: List[str] = Field(description="ウェイポイントのリスト")    transit: str = Field(description="交通手段")

これにより、LLMコールの出力を方向コールに接続できるようになります。

このプロンプトでは、ChatGPT/PaLMからの結果を使用してウェイポイントのリストを抽出するための小規模なオープンソースLLMの微調整が興味深い派生プロジェクトになる可能性があるため、ワンショットの例を追加することでモデルが望ましい出力に合うようになりました。

class MappingTemplate(object):    def __init__(self):        self.system_template = """      詳細な旅行計画をシンプルな場所リストに変換するエージェントです。      旅程は4つのハッシュタグによって示されます。それを訪れるべき場所のリストに変換してください。各場所の具体的な住所を含めるようにしてください。      出発地と到着地は常に出力に含まれている必要があり、ウェイポイントのリストも含まれる場合があります。また、交通手段も含まれる必要があります。ウェイポイントの数は20を超えてはいけません。      交通手段が推測できない場合は、旅行先を考慮して最善の推測を行ってください。      例:      ####      ロンドン内の2日間のドライブ旅行の旅程:      - Day 1:        - Buckingham Palaceでスタート(The Mall, London SW1A 1AA)        - Tower of Londonを訪れる(Tower Hill, London EC3N 4AB)        - British Museumを探索する(Great Russell St, Bloomsbury, London WC1B 3DG)        - Oxford Streetでショッピングを楽しむ(Oxford St, London W1C 1JN)        - Covent Gardenで一日を終える(Covent Garden, London WC2E 8RF)      - Day 2:        - Westminster Abbeyでスタート(20 Deans Yd, Westminster, London SW1P 3PA)        - Churchill War Roomsを訪れる(Clive Steps, King Charles St, London SW1A 2AQ)        - Natural History Museumを探索する(Cromwell Rd, Kensington, London SW7 5BD)        - Tower Bridgeで旅行を終える(Tower Bridge Rd, London SE1 2UP)      #####      出力:      Start: Buckingham Palace, The Mall, London SW1A 1AA      End: Tower Bridge, Tower Bridge Rd, London SE1 2UP      Waypoints: ["Tower of London, Tower Hill, London EC3N 4AB", "British Museum, Great Russell St, Bloomsbury, London WC1B 3DG", "Oxford St, London W1C 1JN", "Covent Garden, London WC2E 8RF","Westminster, London SW1A 0AA", "St. James's Park, London", "Natural History Museum, Cromwell Rd, Kensington, London SW7 5BD"]      Transit: driving      交通手段は次のいずれかである必要があります: "driving", "train", "bus"または"flight"。      {format_instructions}    """        self.human_template = """      ####{agent_suggestion}####    """        self.parser = PydanticOutputParser(pydantic_object=Trip)        self.system_message_prompt = SystemMessagePromptTemplate.from_template(            self.system_template,            partial_variables={                "format_instructions": self.parser.get_format_instructions()            },        )        self.human_message_prompt = HumanMessagePromptTemplate.from_template(            self.human_template, input_variables=["agent_suggestion"]        )        self.chat_prompt = ChatPromptTemplate.from_messages(            [self.system_message_prompt, self.human_message_prompt]        )

さて、Agentクラスに、SequentialChainを使用して順番にItineraryTemplateMappingTemplateを呼び出す新しいメソッドを追加しましょう。

def _set_up_agent_chain(self, debug=True):      # イティネラリーを文字列として取得するためにLLMChainをセットアップ    travel_agent = LLMChain(            llm=self.chat_model,            prompt=self.itinerary_prompt.chat_prompt,            verbose=debug,            output_key="agent_suggestion",        )        # ウェイポイントをJSONオブジェクトとして抽出するためにLLMChainをセットアップ    parser = LLMChain(            llm=self.chat_model,            prompt=self.mapping_prompt.chat_prompt,            output_parser=self.mapping_prompt.parser,            verbose=debug,            output_key="mapping_list",        )         # オーバーオールチェーンは、travel_agentとparserを順番に呼び出すことができるようにします    # ラベル付けされた出力を使用します。    overall_chain = SequentialChain(            chains=[travel_agent, parser],            input_variables=["query", "format_instructions"],            output_variables=["agent_suggestion", "mapping_list"],            verbose=debug,        )    return overall_chain

これらの呼び出しを行うために、次のコードを使用できます。

agent_chain = travel_agent._set_up_agent_chain()mapping_prompt = MappingTemplate()agent_result = agent_chain(                {                    "query": query,                    "format_instructions": mapping_prompt.parser.get_format_instructions(),                }            )trip_suggestion = agent_result["agent_suggestion"]waypoints_dict = agent_result["mapping_list"].dict()

waypoints_dict内のアドレスは、Googleマップで使用するために十分にフォーマットされていますが、方向APIを呼び出す際のエラーの可能性を減らすためにジオコード化することもできます。ウェイポイントのディクショナリは次のようになります。

{'start': 'Berkeley, CA', 'end': 'Seattle, WA', 'waypoints': ['Redwood National and State Parks, 1111 Second St, Crescent City, CA 95531', 'Crater Lake National Park, Crater Lake National Park, OR 97604', 'Mount Rainier National Park, 55210 238th Ave E, Ashford, WA 98304', 'Olympic National Park, 3002 Mount Angeles Rd, Port Angeles, WA 98362'], 'transit': 'driving'}

6. すべてをまとめる

これで、LLMを使用して旅行クエリを検証し、詳細なイティネラリーを生成し、ウェイポイントをJSONオブジェクトとして抽出して使用できるようになりました。コードでは、この機能のほとんどがAgentクラスによって処理されていることに気付くでしょう。これはTravelMapperBase内でインスタンス化され、次のように使用されます。

travel_agent = Agent(   open_ai_api_key=openai_api_key,   google_palm_api_key=google_palm_api_key,   debug=verbose,)itinerary, list_of_places, validation = travel_agent.suggest_travel(query)

LangChainを使用すると、使用されているLLMを簡単に切り替えることができます。PALMの場合、次のように宣言するだけです。

from langchain.llms import GooglePalmAgent.chat_model = GooglePalm(   model_name="models/text-bison-001",   temperature=0,   google_api_key=google_palm_api_key,)

OpenAIの場合は、前述のセクションで説明されているように、ChatOpenAIまたはOpenAIを使用することができます。

さて、次のステージに進んでみましょう:場所のリストを方向に変換し、ユーザーが調べるために地図上にプロットする方法はどうすればいいでしょうか?これについては、この三部作の第2部で説明します。

読んでくださってありがとうございます!コードベース全体をこちらでご覧いただけます:https://github.com/rmartinshort/travel_mapper。機能の改善や拡張に対するご意見や提案があれば、ぜひお知らせください!

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more