「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 APIキーを先に設定した.env
ファイルに追加します
- 「LangChain、Google Maps API、およびGradioを使用したスマートな旅行スケジュール提案システムの構築(パート3)」
- 「プロンプトエンジニアリングに入るための5つの必須スキル」
- 『9月は「セプテムクエイク」? Rを使ったメキシコの地震活動データの分析と可視化』
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_address
とplace_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']
この情報の中で、legs
とoverview_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_dict
とbuild_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が対話型の地図を生成し、各ウェイポイントを探索し、クリックすることができます。
4. ルートの最適化
ウェイポイントのリストを使用してGoogle Mapsの方向APIへの単一の呼び出しを行い、overview_polyline
をプロットするアプローチは、POCとしては素晴らしいですが、いくつかの問題があります:
formatted_address
ではなく、Google Mapsへの呼び出しで開始、終了、およびウェイポイントの名前を指定する際にplace_id
を使用する方が効率的です。幸いなことに、ジオコーディングの呼び出しの結果でplace_id
を取得できるため、それを使用する必要があります。- 単一のAPI呼び出しでリクエストできるウェイポイントの数は25に制限されています(詳細については、https://developers.google.com/maps/documentation/directions/get-directionsを参照)。LLMの旅程に25以上の停留所がある場合、Google Mapsに複数回の呼び出しを行い、レスポンスをマージする必要があります。
overview_polyline
は、ズームインすると解像度が制限されます。これは、それに沿ったポイントの数が大規模な地図ビューに最適化されているためです。POCにはそれほど重大な問題ではありませんが、高倍率で見栄えが良くなるように、ルートのセグメントごとにより詳細なポリラインを使用することができます。- 地図上で、ルートを複数のセクションに分割し、各セクションの距離と所要時間をユーザーに表示できると便利です。ここでもGoogle Mapsはその情報を提供してくれるので、利用するべきです。
Issue 1は、build_directions_and_route
をformatted_address
ではなくmapping_dict
のplace_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として保存することができます。
ほとんどの場合、ルートをリクエストするときに地図を生成したいため、RouteFinder
のgenerate_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
クラスを作成しました。今ではRouteFinder
とAgent
が揃ったので、これらをまとめて旅行マッパープロジェクトのベースクラスに組み合わせることができます。
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!
Was this article helpful?
93 out of 132 found this helpful
Related articles