Connecting AI to Google Places
I have a proof of concept web app called Ready Yup included in my portfolio. You can see a live demo of it here, but what it is, is a demonstration of an integration between a large language model (LLM), and Google's Map and Places APIs. I thought it was an interesting idea, and I wanted to talk a bit about how I made it.
So first of all, let's talk about what I wanted the application to do, because that's what informed my choices for which technologies I used. My idea was for an application that would allow you to get a short list of restaurants in a given area, and to then give you basic information such as distance and an average customer rating. I also then wanted it to produce very short blurb-summaries of the good and bad points of each restaurant, to help a user decide between potential locations at a quick glance.
The first part was easy enough. Google Places supports querying through a RESTful API, and it supports powerful text-based search queries. For example, "Italian restaurants in Manhattan, NY" would work, but so would "Chinese restaurants near 123, Example Street, Example City." It didn't demand a specifically supported format, and that made it extremely appealing. As for how to get the locations, that one seemed even more of an easy-answer: I could use Google Maps. That would give the user a sleek, powerful, familiar interface to work with while inputting their address. I used Svelte to make my life easier, and because, well... it made sense.
The next part required more thought. I knew I wanted to make Ready Yup a web app, so choosing JavaScript as my front end language made sense. That would make connecting to Google Maps virtually free. However, I would be relying on OpenAI as my language model, and that needs to run on the backend. I had the choice between Node.JS and Python for that, and I ultimately went with Python because I figured there would be less overhead. My decision meant that I would be deploying my backend code to a GCP cloud function.
So what did that code look like? Well, on the front end, Google Map's API has a few features that are kind of cool. For example, there's an auto-complete feature that you can connect to an address search box. But largely, I am relying on stock functionality of Google Maps. If you're interested in seeing any of that, I do have the repo uploaded to Git. What I really want to focus on though is what's happening on the backend.
My GCP function exposes an HTTP endpoint, which expects two parameters: filter, and address. The address can be in any format, really, since it will be sorted out by Google's Places API searchText functionality -- but we expect that it comes from Google Maps' autocomplete field. Filter, on the other hand, should be one of a handful of pre-defined tags ("PIZZA", "CHINESE", "BREAKFAST", etc.) The exposed endpoint will do a few things. First, let's look at this code:
def getDataFromSearchText(q, num = 4):
endpoint = "https://places.googleapis.com/v1/places:searchText"
headers = {
'Content-Type': 'application/json',
'X-Goog-Api-Key': API_KEY,
'X-Goog-FieldMask': 'places.displayName,places.id,places.formattedAddress'
}
data = { 'textQuery': q }
response = requests.post(endpoint, headers=headers, json=data)
return [[x['displayName']['text'], x['id'], x['formattedAddress']] for x in response.json()['places'][:num]]
places = getDataFromSearchText(filter_value + " near: " + addr, num)
This will get a JSON object from Google Places containing, among some other information, the unique IDs representing the businesses. This ID can be passed to several other endpoints that are exposed by the API. Notably, it allows us to query for reviews.
def getReviewsFromID(pid):
endpoint = "https://maps.googleapis.com/maps/api/place/details/json?place_id=" + str(pid) + "&fields=reviews&key=" + API_KEY
response = requests.get(endpoint)
review_json = response.json()
reviews = []
try:
for each_review in review_json['result']['reviews']:
reviews.append([each_review['rating'], each_review['text']])
except:
pass
return reviews
Here is where things start to get interesting though. So far, our cloud function has been little more than a bridge to the Google Places API. Risk of exposing our API key aside, there's nothing we couldn't have just done on the front end. However, our backend is also creating an OpenAI client object. We'll be using that to do some heavy lifting real soon, and in order to do that, we first write a function that allows us to generate text. If you've never done this before, you might be surprised at how simple the boilerplate code actually is. But here it is:
client = OpenAI(api_key=OPENAI_API_KEY)
def runBasicLLMTask(query):
chat_completion = client.chat.completions.create(
messages=[{ "role": "user", "content": query }],
model="gpt-4",
)
return chat_completion.choices[0].message.content
And that's most of the pieces! All that's left is to create a function that takes the reviews returned from our previously defined function, and which aggregates and summarizes them. This is where the language learning task actually comes into play.
def aggregateReviews(reviews):
ret_dict = {"pro1" : "NULL", "pro2" : "NULL", "con1" : "NULL", "con2" : "NULL" }
try:
avg = sum([x[0] for x in reviews])/len(reviews)
combined_text = "".join(["Review: " + x[1] + " " for x in reviews])
ai_prompt = "Your answer should be a JSON object. It should have the following keys: pro1, pro2, con1, con2. \n\
The value of pro1 should be a 10 word or less good things about the business. \n\
The value of pro2 should be another 10 word or less good thing about the business. \n\
The value of con1 should be a 10 word or less negative of the business. \n\
The value of con2 should be another 10 word or less negative of the business. \n\
If there isn't enough information to conclude one of these values, make that value the string 'NULL'. \n\
If the review is blank, or you don't have any reviews, then all values of the JSON should be the string 'NULL'. \n\
Under no circumstances should your answer ever include more than just this JSON object. Never elaborate or explain it. \n\
Base your summary on the following reviews: " + combined_text
agg = runBasicLLMTask(ai_prompt)
ret_dict = json.loads(agg)
ret_dict['avg'] = avg
except:
pass
return ret_dict
And that's how it works! The "code" is very self-explanatory, because... well, it's mostly just English. One point worth noting is that we do have the LLM directly generate a JSON object. That's something that it's capable of doing as long as our instructions have enough specificity.
I will point out that there are a few other pieces floating around in the codebase. You'll have realized that I haven't actually talked about getting photos of the businesses, or calculating distance between locations. I didn't think those were the exciting parts of the project, but the code is freely available on Git if you'd like to take a look. Hopefully this blog post did help to elucidate some of the more interesting bits, though.
You can check out the Ready Yup source code: here
Comments
Post a Comment