ChatGPT・GoogleMapsで観光プランを生成する方法


背景

前回は、GoogleMaps APIの結果をFoliumで可視化してみました。

GoogleMaps APIの結果をFoliumで可視化してみる|harukary

今回は、ChatGPT×GoogleMapsで、観光プランを作成してみたいと思います。

アプローチ

ChatGPT APIで観光地の情報を出力し、その観光地を回る経路をGoogleMaps APIを使って出力します。以下の内容の応用編、と言った感じです。

サンプルコード

Prerequisites

いつも通り、APIキーを読み込みます。OPENAI_API_KEYとGOOGLE_MAPS_API_KEYを読み込みます。

# !pip install googlemaps folium openai langchain
from dotenv import find_dotenv,load_dotenv
load_dotenv(find_dotenv())

List up attractions using LLM

今回は、LangChainの以下の機能を使います。LLMから任意のデータを抽出するのは、LLMChain×PydanticOutputParserでいけますね。

from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.chains import LLMChain
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel,Field
from typing import Optional

LLMChainの構成要素を作っていきます。

llm = ChatOpenAI(model='gpt-3.5-turbo-16k',temperature=0.1)

template = """\
## Instruction
List up {k} attractions in {place}.
{user_profile}

## Output
{format_instruction}
"""

class Attraction(BaseModel):
    name: str = Field(description='The name of attraction')
    country: str = Field(description='The country the attraction located')
    city: str = Field(description='The city the attraction located')

    description: str = Field(description='Description of the attraction. More than 1000 words.')
    duration: float = Field(description='Duration time of the attraction. Hours.')

    lat: Optional[float] = Field(description='latitude of the attraction')
    lng: Optional[float] = Field(description='longitude of the attraction')

class Attractions(BaseModel):
    data: list[Attraction] = Field(description='List of Atrractions')

parser = PydanticOutputParser(pydantic_object=Attractions)

prompt = ChatPromptTemplate.from_template(
    template=template,
    partial_variables={'format_instruction':parser.get_format_instructions()},
)

地名に対してk個の観光スポット(Attraction)をリストアップします。また、ユーザプロファイルを入力することで、出力をパーソナライズします。観光スポットのデータは、名前、国、街、1000単語以上の解説、所要時間、緯度・経度を含ませます。

Pydanticのデータのリストで返してほしい場合は、複数形のクラスを作り、元のクラスのリストを置きます。

これで、LLMChainを構成できます。

chain = LLMChain(llm=llm,prompt=prompt,output_parser=parser)#,verbose=True)

今年新婚旅行で初めてロンドンに行くので、ロンドンの観光スポットをリストアップしてもらいます。

user_profile = "This is my first visit in here."
res = chain.run(place='London',k=3,user_profile=user_profile)

Attractionsは、dataにAttractionのリストを持っているので、以下でデータを取得します。

data = res.dict()['data']
data[0]
{'name': 'Tower of London',
 'country': 'United Kingdom',
 'city': 'London',
 'description': 'The Tower of London is a historic castle located on the north bank of the River Thames in central London. It was founded towards the end of 1066 as part of the Norman Conquest of England. The White Tower, which gives the entire complex its name, was built by William the Conqueror in 1078 and was a resented symbol of oppression, inflicted upon London by the new ruling elite. The castle was used as a prison from 1100 until 1952, although that was not its primary purpose. A grand palace early in its history, it served as a royal residence. As a whole, the Tower is a complex of several buildings set within two concentric rings of defensive walls and a moat. There were several phases of expansion, mainly under Kings Richard the Lionheart, Henry III, and Edward I in the 12th and 13th centuries. The general layout established by the late 13th century remains despite later activity on the site.',
 'duration': 2.0,
 'lat': 51.5081,
 'lng': -0.0761}

いい感じですね。解説を翻訳したものを以下に載せておきます。Tower of Londonの歴史の説明ですね。観光スポットとしての説明を生成するような指示に調整する必要があるかもしれません。

ロンドン塔は、ロンドン中心部のテムズ川北岸に位置する歴史的な城塞です。これは1066年の終わりごろに、イングランドのノルマン征服の一部として設立されました。その名前の由来となったホワイトタワーは、ウィリアム征服王によって1078年に建設され、新しい支配階級によってロンドンに強制された抑圧の象徴として嫌われていました。城塞は1100年から1952年まで刑務所として使われていましたが、それが主な目的ではありませんでした。歴史の初期には壮大な宮殿であり、王室の居住地として使われていました。全体として、ロンドン塔は防御壁と堀に囲まれたいくつかの建物の複合体です。主にリチャード獅子心王、ヘンリー3世、エドワード1世の時代の12世紀と13世紀に拡張の段階が何度もありました。13世紀末に確立された一般的なレイアウトは、その後のサイト上の活動にもかかわらず残っています。

緯度・経度が入っていますが、ここは、GoogleMaps APIを使って書き換えるのでOKです。他には、British Museum、Buckingham Palaceが出力されました。

Get latitude and longitude using GoogleMaps API

次に、GoogleMaps APIを使って緯度・経度を更新します。

import googlemaps,os,json
gmaps = googlemaps.Client(key=os.environ['GOOGLE_MAPS_API_KEY'])

気になったので、ChatGPT出力と緯度・経度を比べながら、GoogleMaps出力を代入していきます。

for i,d in enumerate(data):
    print(i,d['name'],'-'*30)
    geocode_result = gmaps.geocode(d['name'])
    gmap_res = geocode_result[0]["geometry"]["location"]
    print('GoogleMaps:',gmap_res["lat"],gmap_res["lng"])
    print('ChatGPT:',d["lat"],d["lng"])
    d['lat'],d['lng'] = gmap_res['lat'],gmap_res['lng']
0 Tower of London ------------------------------
GoogleMaps: 51.50811239999999 -0.0759493
ChatGPT: 51.5081 -0.0761
1 British Museum ------------------------------
GoogleMaps: 51.5194133 -0.1269566
ChatGPT: 51.5194 -0.1269
2 Buckingham Palace ------------------------------
GoogleMaps: 51.501364 -0.14189
ChatGPT: 51.5014 -0.1419

なんと、かなり近いですね。あまり間違っていない気がします。ChatGPTで安定して出力される数値情報(緯度・経度や、食品の成分など)がどれくらい信頼できるのかは気になりますね。全く信頼できないならいいんですが、結構信頼できそうなものも多いので。

おそらく、ロンドンが世界的に人気な観光地であるため、正確に出てきているのだと思います。日本のマイナーな観光地などでは、GoogleMaps APIを使ったほうがいいと思います。

ということで、日本のマイナーな観光地で試します。

  • 岐阜の場合:これでもいい感じだ、、

0 Gifu Castle ------------------------------
GoogleMaps: 35.4340233 136.7820544
ChatGPT: 35.4229 136.7606
1 Takayama Old Town ------------------------------
GoogleMaps: 36.1413161 137.2596227
ChatGPT: 36.1408 137.2513
2 Shirakawa-go ------------------------------
GoogleMaps: 36.2710289 136.8985744
ChatGPT: 36.2708 136.9075
  • 愛媛の場合:これもだいぶ正確。。

0 Matsuyama Castle ------------------------------
GoogleMaps: 33.8455768 132.7655346
ChatGPT: 33.8394 132.7657
1 Dogo Onsen ------------------------------
GoogleMaps: 33.8520419 132.7864065
ChatGPT: 33.8485 132.7826
2 Shimanami Kaido ------------------------------
GoogleMaps: 34.2603674 133.0881146
ChatGPT: 34.0667 133.0

ということで、必須ではないのかもしれません。まあどちらにしろ、もっと詳細な地点データを使う場合は、GoogleMapsを使う必要がありますけどね。

Get directions using GoogleMaps API

以下のサイトのdecode_polyline()を使って、PolyLineをデコードします。

Google Maps APIのPolyLineをデコードする関数(Python3) - Qiita

では、9:00に出発して30分移動して最初の目的地に行くと仮定し、旅行計画を立ててみます。簡単のため、休憩・昼食は抜きで、観光してもらいます。

from datetime import datetime, timedelta
now = datetime.strptime('2023/08/01 09:00','%Y/%m/%d %H:%M')

routes = []
first_trans_min = 30
print(now.strftime('%H:%M'), f"Going to {data[0]['name']}")
print('  ',f'Duration {str(int(first_trans_min))} minutes')
now += timedelta(minutes=first_trans_min)

for i in range(len(data)-1):
    directions_result = gmaps.directions(
        data[i]['name'],
        data[i+1]['name'],
        mode="transit",
        departure_time=now
    )
    duration_attraction_hour = data[i]['duration']
    print(now.strftime('%H:%M'), data[i]['name'])
    print('  ',f'Spent {"{:.1f}".format(duration_attraction_hour)} hours')
    now += timedelta(hours=duration_attraction_hour)
    
    duration_direction_sec = directions_result[0]['legs'][0]['duration']['value']
    print(now.strftime('%H:%M'), f"Going to {data[i]['name']}")
    print('  ',f'Duration {str(int(duration_direction_sec/60))} minutes')
    now += timedelta(seconds=duration_direction_sec)
    
    steps = []
    for j,s in enumerate(directions_result[0]['legs'][0]['steps']):
        steps.append({
            'polyline': decode_polyline(s['polyline']['points']),
            'travel_mode': s['travel_mode'],
            'duration': s['duration']['text'],
            'duration_sec': s['duration']['value']
        })
    routes.append(steps)
    # break
print(now.strftime('%H:%M'), "End")
09:00 Going to Tower of London
   Duration 30 minutes
09:30 Tower of London
   Spent 2.0 hours
11:30 Going to Tower of London
   Duration 30 minutes
12:00 British Museum
   Spent 3.0 hours
15:00 Going to British Museum
   Duration 18 minutes
15:18 End

いい感じにプランができました。GoogleMapsのDirectionsは、アプリ同様、出発時刻や到着時刻を指定できます。そのため、ここでも、2023/8/1の各時刻の場合の最適な経路が計算できています。

最後に、地図にこのプランの経路を可視化します。徒歩は青、公共交通は赤で表示します。

import folium
from branca.element import Figure
import pandas as pd
import numpy as np

color_map = {
    'WALKING': '#0000ff',
    'TRANSIT': '#ff0000',
}
lat = np.mean([d['lat'] for d in data],)
lng = np.mean([d['lng'] for d in data],)
map = folium.Map(location=[lat,lng], zoom_start=14)

for d in data:
    folium.Marker(location=[d['lat'], d['lng']], popup=f"{d['name']} ({d['duration']})").add_to(map)

for i,steps in enumerate(routes):
    for step in steps:
        # print(len(step['polyline']))
        folium.vector_layers.PolyLine(
            locations=step['polyline'], popup=f'{step["travel_mode"]} ({step["duration"]})', color=color_map[step['travel_mode']]
        ).add_to(map)
    # break

fig = Figure(width=800, height=600)
fig.add_child(map)
ロンドン観光プラン

いい感じ。。ここまで1時間くらいでできました。すごい。。

まとめ

ChatGPT×GoogleMapsで、思ったより簡単に観光プラン作成ができました。

ChatGPTの力はまだ全然使えてないですが、ChatGPT×GoogleMapsの可能性が見えた感じがします。やっぱり、対話に基づいてユーザプロファイルを作成・好みに合った観光プランを作成するとか、状況に応じてプランを修正するなどの機能が作ると、ChatGPTの力を活かせそうです。

参考

GoogleMaps APIの結果をFoliumで可視化してみる|harukary
【DLAI×LangChain講座①】 Models, Prompts and Output Parsers|harukary
【DLAI×LangChain講座③】 Chains|harukary
Google Maps APIのPolyLineをデコードする関数(Python3) - Qiita

サンプルコード

https://github.com/harukary/llm_samples/blob/main/LangChain/other/googlemaps/travel_plan_generator.ipynb

この記事が気に入ったらサポートをしてみませんか?