「LangChain、Google Maps API、およびGradioを使用したスマートな旅行スケジュール提案システムの構築(パート2)」

「LangChain、Google Maps API、およびGradioを利用したスマートな旅行スケジュール提案システムの構築(パート2)」

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

この記事は、OpenAIとGoogleのAPIを使用して旅行の行程を提案するアプリケーションを作成し、gradioで生成されたシンプルなUIで表示する、3部作のうちの2つ目です。このパートでは、Google Maps APIとfoliumを使用して、ウェイポイントのリストからインタラクティブなルートマップを生成する方法について説明します。コードだけを見たい場合は、こちらをご覧ください

1. パート1のまとめ

この3部作の最初のパートでは、LangChainとプロンプトエンジニアリングを使用して、GoogleのPaLMまたはOpenAIのChatGPTとの連続した呼び出しによって、ユーザーのクエリを旅程とアドレスのリストに変換するシステムを構築しました。さあ、そのアドレスのリストを取得し、地図上にプロットされた旅行ルートに変換する方法を見ていきましょう。これには、主にGoogle Maps APIを使用する予定です。また、プロットにはfoliumも使用します。では、始めましょう!

2. API呼び出しの準備

Google MapsのAPIキーを作成するには、まずGoogle Cloudでアカウントを作成する必要があります。その後、90日間の無料トライアル期間があり、その後はOpenAIと同様にAPIを使用したサービスに対して支払いを行います。それが完了したら、プロジェクト(私の場合はLLMMapper)を作成し、Google CloudサイトのGoogle Maps Platformセクションに移動します。そこから、「Keys & Credentials」メニューにアクセスしてAPIキーを生成できるはずです。また、「APIs & Services」メニューをチェックして、Google Maps Platformが提供する多くのサービスをご覧いただくこともおすすめです。このプロジェクトでは、DirectionsとGeocodingサービスのみを使用します。ウェイポイントごとにジオコーディングを行い、その後それらの間の方向を検索します。

Google Maps PlatformサイトのKeys & Credentialsメニューへのナビゲーションを示すスクリーンショット。ここでAPIキーを作成します。

それでは、Google Maps APIキーを先に設定した.envファイルに追加します

OPENAI_API_KEY = {あなたのOpenAIキー}GOOGLE_PALM_API_KEY = {あなたのGoogle Palm APIキー}GOOGLE_MAPS_API_KEY = {あなたのGoogle Maps APIキー}

これが動作するかテストするには、.envから秘密情報を読み込んで、パート1で説明した方法でシークレットをロードします。次に、以下のようにジオコーディング呼び出しを試みることができます

import googlemapsdef convert_to_coords(input_address):    return self.gmaps.geocode(input_address)secrets = load_secets()gmaps = googlemaps.Client(key=secrets["GOOGLE_MAPS_API_KEY"])example_coords = convert_to_coords("The Washington Moment, DC")

Google Mapsは、提供された文字列を実際の場所の住所や詳細と一致させることができ、次のようなリストを返すはずです

[{'address_components': [{'long_name': '2',    'short_name': '2',    'types': ['street_number']},   {'long_name': '15th Street Northwest',    'short_name': '15th St NW',    'types': ['route']},   {'long_name': 'Washington',    'short_name': 'Washington',    'types': ['locality', 'political']},   {'long_name': 'District of Columbia',    'short_name': 'DC',    'types': ['administrative_area_level_1', 'political']},   {'long_name': 'United States',    'short_name': 'US',    'types': ['country', 'political']},   {'long_name': '20024', 'short_name': '20024', 'types': ['postal_code']}],  'formatted_address': '2 15th St NW, Washington, DC 20024, USA',  'geometry': {'location': {'lat': 38.8894838, 'lng': -77.0352791},   'location_type': 'ROOFTOP',   'viewport': {'northeast': {'lat': 38.89080313029149,     'lng': -77.0338224697085},    'southwest': {'lat': 38.8881051697085, 'lng': -77.0365204302915}}},  'partial_match': True,  'place_id': 'ChIJfy4MvqG3t4kRuL_QjoJGc-k',  'plus_code': {'compound_code': 'VXQ7+QV Washington, DC',   'global_code': '87C4VXQ7+QV'},  'types': ['establishment',   'landmark',   'point_of_interest',   'tourist_attraction']}]

これは非常にパワフルです!リクエストは多少曖昧ですが、Googleマップサービスは、正確な住所と座標、およびアプリケーションに依存する開発者に有用な場所情報を正しくマッチングしました。ここではformatted_addressplace_idのフィールドのみを使用する必要があります。

3. ルートの作成

ジオコーディングは、旅行マッピングアプリケーションにとって重要です。ジオコーディングAPIは、方向APIよりも、曖昧または部分的に完全でない住所を扱う能力に優れています。LLMコールからの住所が、方向APIに適切な応答を与えるための十分な情報を含んでいるという保証はありませんので、このジオコーディングステップを最初に行うことでエラーの可能性を減らすことができます。

まず、出発地点、到着地点、そして経由地点のリスト上でジオコーディングを呼び出し、結果を辞書に格納しましょう。

   def build_mapping_dict(start, end, waypoints):    mapping_dict = {}    mapping_dict["start"] = self.convert_to_coords(start)[0]    mapping_dict["end"] = self.convert_to_coords(end)[0]        if waypoints:      for i, waypoint in enumerate(waypoints):          mapping_dict["waypoint_{}".format(i)] = convert_to_coords(                    waypoint                )[0    return mapping_dict

今度は、出発地から到着地までのルートを取得するために方向APIを利用できます。

    def build_directions_and_route(        mapping_dict, start_time=None, transit_type=None, verbose=True    ):    if not start_time:        start_time = datetime.now()    if not transit_type:        transit_type = "driving"            # これは後でもより効率の良いplace_idに置き換えます      waypoints = [            mapping_dict[x]["formatted_address"]            for x in mapping_dict.keys()            if "waypoint" in x      ]      start = mapping_dict["start"]["formatted_address"]      end = mapping_dict["end"]["formatted_address"]      directions_result = gmaps.directions(            start,            end,            waypoints=waypoints,            mode=transit_type,            units="metric",            optimize_waypoints=True,            traffic_model="best_guess",            departure_time=start_time,      )      return directions_result

方向APIの完全なドキュメントはこちらにあり、指定できるさまざまなオプションがあります。ルートの開始と終了、および経由地点のリストを指定し、optimize_waypoints=Trueを選択することで、Googleマップにウェイポイントの順序を変更して合計の移動時間を短縮することができることに注意してください。また、デフォルトではdrivingに設定されるトランジットタイプも指定できます。このように、第1部でLLMにトランジットタイプを要求したので、理論的にはここでも利用することができるかもしれません。

方向APIの呼び出しで返される辞書には、以下のキーが含まれています。

['bounds', 'copyrights', 'legs', 'overview_polyline', 'summary', 'warnings', 'waypoint_order']

この情報の中で、legsoverview_polylineが最も役に立ちます。legsはルートセグメントのリストで、各要素は次のようになります。

['distance', 'duration', 'end_address', 'end_location', 'start_address', 'start_location', 'steps', 'traffic_speed_entry', 'via_waypoint']

legはさらにstepsに分割されており、これはターンバイターンの指示とそれに関連するルートセグメントのコレクションです。これは、次のキーを持つ辞書のリストです。

['distance', 'duration', 'end_location', 'html_instructions', 'polyline', 'start_location', 'travel_mode']

polylineキーには実際のルート情報が格納されています。各ポリラインは、Googleマップが長い緯度経度のリストを文字列に圧縮する手段として生成する、座標のエンコードされた表現です。これらは、次のようなエンコードされた文字列です

“e|peFt_ejVjwHalBzaHqrAxeE~oBplBdyCzpDif@njJwaJvcHijJ~cIabHfiFyqMvkFooHhtE}mMxwJgqK”

詳細はこちらを読んでくださいが、幸いにも、decode_polylineユーティリティを使用してそれらを座標に変換することができます。例えば:

from googlemaps.convert import decode_polylineoverall_route = decode_polyline(directions_result[0]["overview_polyline"]["points"])route_coords = [(float(p["lat"]),float(p["lng"])) for p in overall_route]

これにより、ルートに沿った緯度と経度のポイントのリストが得られます。

これだけで、ルート上のウェイポイントとそれらを接続する正しい運転経路を示すシンプルな地図を描画するために必要な情報がすべて揃います。開始点としてoverview_polylineを使用できますが、後で、地図の高倍率で解像度の問題が発生することを確認することがあります。

次のクエリから始まったと仮定しましょう:

「サンフランシスコからラスベガスへの5日間のロードトリップをしたい。HW1沿いの美しい沿岸の町を訪れ、それからカリフォルニア南部の山岳景観を堪能したい」

LLM calls では、ウェイポイントの辞書を抽出し、Google Mapsからの方向とルートを取得するためにbuild_mapping_dictbuild_directions_and_routeを実行しました。

まず、次のようにウェイポイントを抽出できます:

marker_points = []nlegs = len(directions_result[0]["legs"])for i, leg in enumerate(directions_result[0]["legs"]):  start, start_address = leg["start_location"], leg["start_address"]  end,  end_address = leg["end_location"], leg["end_address"]  start_loc = (float(start["lat"]),float(start["lng"]))  end_loc = (float(end["lat"]),float(end["lng"]))  marker_points.append((start_loc,start_address))  if i == nlegs-1:    marker_points.append((end_loc,end_address))

このようにしてウェイポイントを抽出した後、foliumとbrancaを使用して、ColabまたはJupyter Notebookで表示される見栄えの良い対話型地図を描画することができます。

import foliumfrom branca.element import Figurefigure = Figure(height=500, width=1000)# ルートをデコードoverall_route = decode_polyline(  directions_result[0]["overview_polyline"]["points"])route_coords = [(float(p["lat"]),float(p["lng"])) for p in overall_route]# マップの中心をルートの開始位置に設定map_start_loc = [overall_route[0]["lat"],overall_route[0]["lng"]]map = folium.Map(  location=map_start_loc,   tiles="Stamen Terrain",   zoom_start=9)figure.add_child(map)# ウェイポイントを赤のマーカーとして追加for location, address in marker_points:    folium.Marker(        location=location,        popup=address,        tooltip="<strong>クリックして住所を表示</strong>",        icon=folium.Icon(color="red", icon="info-sign"),    ).add_to(map)# ルートを青い線として追加f_group = folium.FeatureGroup("ルート概要")folium.vector_layers.PolyLine(    route_coords,    popup="<b>ルート全体</b>",    tooltip="ここに距離と所要時間を追加できるツールチップ",    color="blue",    weight=2,).add_to(f_group)f_group.add_to(map)

このコードを実行すると、Foliumが対話型の地図を生成し、各ウェイポイントを探索し、クリックすることができます。

Google Maps APIの結果から生成された対話型地図

4. ルートの最適化

ウェイポイントのリストを使用してGoogle Mapsの方向APIへの単一の呼び出しを行い、overview_polylineをプロットするアプローチは、POCとしては素晴らしいですが、いくつかの問題があります:

  1. formatted_addressではなく、Google Mapsへの呼び出しで開始、終了、およびウェイポイントの名前を指定する際にplace_idを使用する方が効率的です。幸いなことに、ジオコーディングの呼び出しの結果でplace_idを取得できるため、それを使用する必要があります。
  2. 単一のAPI呼び出しでリクエストできるウェイポイントの数は25に制限されています(詳細については、https://developers.google.com/maps/documentation/directions/get-directionsを参照)。LLMの旅程に25以上の停留所がある場合、Google Mapsに複数回の呼び出しを行い、レスポンスをマージする必要があります。
  3. overview_polylineは、ズームインすると解像度が制限されます。これは、それに沿ったポイントの数が大規模な地図ビューに最適化されているためです。POCにはそれほど重大な問題ではありませんが、高倍率で見栄えが良くなるように、ルートのセグメントごとにより詳細なポリラインを使用することができます。
  4. 地図上で、ルートを複数のセクションに分割し、各セクションの距離と所要時間をユーザーに表示できると便利です。ここでもGoogle Mapsはその情報を提供してくれるので、利用するべきです。
概略ポリラインの解像度は制限されています。ここではサンタバーバラにズームしており、どの道路を取るべきかは明らかではありません。

Issue 1は、build_directions_and_routeformatted_addressではなくmapping_dictplace_idを使用するように修正することで簡単に解決できます。Issue 2は、初期のウェイポイントを最大長さのチャンクに分割し、それぞれの開始、終了、およびサブリストのウェイポイントを作成し、それらに対してbuild_mapping_dictおよびbuild_directions_and_routeを実行する必要があります。結果は最後に連結されます。

Issue 3および4は、Googleマップが返すルートの各レッグに対して個別のステップポリラインを使用することで解決できます。これにより、これらの2つのレベルを繰り返し処理し、関連するポリラインをデコードし、新しい辞書を構築できます。これにより、プロットに使用されるデコードされたレッグごとに距離と所要時間の値を抽出することも可能になります。

def get_route(directions_result):    waypoints = {}    for leg_number, leg in enumerate(directions_result[0]["legs"]):        leg_route = {}                distance, duration = leg["distance"]["text"], leg["duration"]["text"]        leg_route["distance"] = distance        leg_route["duration"] = duration        leg_route_points = []                for step in leg["steps"]:             decoded_points = decode_polyline(step["polyline"]["points"])            for p in decoded_points:              leg_route_points.append(f'{p["lat"]},{p["lng"]}')            leg_route["route"] = leg_route_points            waypoints[leg_number] = leg_route    return waypoints

問題は、leg_route_pointsリストが非常に長くなる可能性があり、地図上でプロットしようとするとfoliumがクラッシュするか非常に遅くなることです。解決策は、ルート上のポイントをサンプリングして、良好な可視化が可能なだけの数のポイントを持つようにすることですが、地図の読み込みに問題が生じないようにしましょう。

これを行うための単純で安全な方法は、合計ルートに含まれるポイントの数を計算し(例えば5000ポイント)、各レッグに属する割合を決定し、対応する数のポイントを均等にサンプリングすることです。ただし、マップに含まれるためには、各レッグに少なくとも1つのポイントが含まれていることを確認する必要があります。

以下の関数は、get_route関数からのwaypointsの出力を受け取り、このサンプリングを行います。

def sample_route_with_legs(route, distance_per_point_in_km=0.25)):        all_distances = sum([float(route[i]["distance"].split(" ")[0]) for i in route])    # Total points in the sample    npoints = int(np.ceil(all_distances / distance_per_point_in_km))        # Total points per leg    points_per_leg = [len(v["route"]) for k, v in route.items()]    total_points = sum(points_per_leg)    # get number of total points that need to be represented on each leg    number_per_leg = [      max(1, np.round(npoints * (x / total_points), 0)) for x in points_per_leg      ]    sampled_points = {}    for leg_id, route_info in route.items():        total_points = int(points_per_leg[leg_id])        total_sampled_points = int(number_per_leg[leg_id])        step_size = int(max(total_points // total_sampled_points, 1.0))        route_sampled = [                route_info["route"][idx] for idx in range(0, total_points, step_size)            ]        distance = route_info["distance"]        duration = route_info["duration"]        sampled_points[leg_id] = {                "route": [                    (float(x.split(",")[0]), float(x.split(",")[1]))                    for x in route_sampled                ],                "duration": duration,                "distance": distance,            }    return sampled_points

ここでは、望ましいポイント間の間隔を指定し、それに応じてポイントの数を選択します。ルートの長さから望ましいポイント間の間隔を推定する方法も考慮することができますが、この方法は初めの段階ではかなりうまく機能し、地図の中程度の高いズームレベルで受け入れ可能な解像度を提供します。

サンプルポイントの数を理にかなった数の足に分割したので、これらを地図上にプロットし、各足を次のコードでラベル付けできます。

for leg_id, route_points in sampled_points.items():    leg_distance = route_points["distance"]    leg_duration = route_points["duration"]    f_group = folium.FeatureGroup("Leg {}".format(leg_id))    folium.vector_layers.PolyLine(                route_points["route"],                popup="<b>ルートセグメント {}</b>".format(leg_id),                tooltip="距離:{}、所要時間:{}".format(leg_distance, leg_duration),                color="blue",                weight=2,    ).add_to(f_group)    # assumes the map has already been generated    f_group.add_to(map)
地図に表示されるようにラベル付けと注釈が施されたルートの一部の例

5. まとめる

コードベースでは、上記で説明したすべての方法論が2つのクラスにパッケージ化されています。最初のクラスはRouteFinderで、Agent(パート1を参照)の構造化された出力を受け取り、サンプルされたルートを生成します。2番目のクラスはRouteMapperで、サンプルされたルートを受け取り、folium地図をプロットし、htmlとして保存することができます。

ほとんどの場合、ルートをリクエストするときに地図を生成したいため、RouteFindergenerate_routeメソッドはこれらのタスクの両方を処理します。

class RouteFinder:    MAX_WAYPOINTS_API_CALL = 25    def __init__(self, google_maps_api_key):        self.logger = logging.getLogger(__name__)        self.logger.setLevel(logging.INFO)        self.mapper = RouteMapper()        self.gmaps = googlemaps.Client(key=google_maps_api_key)    def generate_route(self, list_of_places, itinerary, include_map=True):        self.logger.info("# " * 20)        self.logger.info("提案された行程")        self.logger.info("# " * 20)        self.logger.info(itinerary)        t1 = time.time()        directions, sampled_route, mapping_dict = self.build_route_segments(            list_of_places        )        t2 = time.time()        self.logger.info("ルートの構築にかかった時間:{}".format((round(t2 - t1, 2))))        if include_map:            t1 = time.time()            self.mapper.add_list_of_places(list_of_places)            self.mapper.generate_route_map(directions, sampled_route)            t2 = time.time()            self.logger.info("地図の生成にかかった時間:{}".format((round(t2 - t1, 2))))        return directions, sampled_route, mapping_dict

パート1では、LLMコールを処理するAgentクラスを作成しました。今ではRouteFinderAgentが揃ったので、これらをまとめて旅行マッパープロジェクトのベースクラスに組み合わせることができます。

class TravelMapperBase(object):    def __init__(        self, openai_api_key, google_palm_api_key, google_maps_key, verbose=False    ):        self.travel_agent = Agent(            open_ai_api_key=openai_api_key,            google_palm_api_key=google_palm_api_key,            debug=verbose,        )        self.route_finder = RouteFinder(google_maps_api_key=google_maps_key)    def parse(self, query, make_map=True):        itinerary, list_of_places, validation = self.travel_agent.suggest_travel(query)        directions, sampled_route, mapping_dict = self.route_finder.generate_route(            list_of_places=list_of_places, itinerary=itinerary, include_map=make_map        )

次のように、クエリ上でこれを実行できます。これはtest_without_gradioスクリプトで与えられた例です。

from travel_mapper.TravelMapper import load_secrets, assert_secretsfrom travel_mapper.TravelMapper import TravelMapperBasedef test(query=None):    secrets = load_secrets()    assert_secrets(secrets)    if not query:        query = """        I want to do 2 week trip from Berkeley CA to New York City.        I want to visit national parks and cities with good food.        I want use a rental car and drive for no more than 5 hours on any given day.        """    mapper = TravelMapperBase(        openai_api_key=secrets["OPENAI_API_KEY"],        google_maps_key=secrets["GOOGLE_MAPS_API_KEY"],        google_palm_api_key=secrets["GOOGLE_PALM_API_KEY"],    )    mapper.parse(query, make_map=True)

ルートと地図の生成に関しては、これで完了です!しかし、このコードをまとめて使いやすいUIにどのようにパッケージングできるでしょうか?それはこのシリーズの第3部でカバーされます。

読んでいただきありがとうございます!こちらでフルのコードベースをご覧いただけます。改善や機能の拡張に対するご提案は大歓迎です!

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