From b7f2061736ce6009b9a1b50f92e10d2d4f49c5b3 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Thu, 20 Apr 2023 07:57:07 -0700 Subject: [PATCH] Harrison/google places (#3207) Co-authored-by: Cao Hoang <65607230+cnhhoang850@users.noreply.github.com> Co-authored-by: vowelparrot <130414180+vowelparrot@users.noreply.github.com> --- .../agents/tools/examples/google_places.ipynb | 105 ++++++++++++++++ langchain/tools/__init__.py | 2 + langchain/tools/google_places/__init__.py | 1 + langchain/tools/google_places/tool.py | 27 +++++ langchain/utilities/__init__.py | 2 + langchain/utilities/google_places_api.py | 112 ++++++++++++++++++ 6 files changed, 249 insertions(+) create mode 100644 docs/modules/agents/tools/examples/google_places.ipynb create mode 100644 langchain/tools/google_places/__init__.py create mode 100644 langchain/tools/google_places/tool.py create mode 100644 langchain/utilities/google_places_api.py diff --git a/docs/modules/agents/tools/examples/google_places.ipynb b/docs/modules/agents/tools/examples/google_places.ipynb new file mode 100644 index 00000000..68a398ff --- /dev/null +++ b/docs/modules/agents/tools/examples/google_places.ipynb @@ -0,0 +1,105 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "487607cd", + "metadata": {}, + "source": [ + "# Google Places\n", + "\n", + "This notebook goes through how to use Google Places API" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "8690845f", + "metadata": {}, + "outputs": [], + "source": [ + "#!pip install googlemaps" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "fae31ef4", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "os.environ[\"GPLACES_API_KEY\"] = \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "abb502b3", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools import GooglePlacesTool" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "a83a02ac", + "metadata": {}, + "outputs": [], + "source": [ + "places = GooglePlacesTool()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "2b65a285", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"1. Delfina Restaurant\\nAddress: 3621 18th St, San Francisco, CA 94110, USA\\nPhone: (415) 552-4055\\nWebsite: https://www.delfinasf.com/\\n\\n\\n2. Piccolo Forno\\nAddress: 725 Columbus Ave, San Francisco, CA 94133, USA\\nPhone: (415) 757-0087\\nWebsite: https://piccolo-forno-sf.com/\\n\\n\\n3. L'Osteria del Forno\\nAddress: 519 Columbus Ave, San Francisco, CA 94133, USA\\nPhone: (415) 982-1124\\nWebsite: Unknown\\n\\n\\n4. Il Fornaio\\nAddress: 1265 Battery St, San Francisco, CA 94111, USA\\nPhone: (415) 986-0100\\nWebsite: https://www.ilfornaio.com/\\n\\n\"" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "places.run(\"al fornos\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66d3da8a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/langchain/tools/__init__.py b/langchain/tools/__init__.py index 44f225e1..3c034f83 100644 --- a/langchain/tools/__init__.py +++ b/langchain/tools/__init__.py @@ -2,6 +2,7 @@ from langchain.tools.base import BaseTool from langchain.tools.ddg_search.tool import DuckDuckGoSearchTool +from langchain.tools.google_places.tool import GooglePlacesTool from langchain.tools.ifttt import IFTTTWebhook from langchain.tools.openapi.utils.api_models import APIOperation from langchain.tools.openapi.utils.openapi_utils import OpenAPISpec @@ -13,5 +14,6 @@ __all__ = [ "AIPluginTool", "OpenAPISpec", "APIOperation", + "GooglePlacesTool", "DuckDuckGoSearchTool", ] diff --git a/langchain/tools/google_places/__init__.py b/langchain/tools/google_places/__init__.py new file mode 100644 index 00000000..e5b5d504 --- /dev/null +++ b/langchain/tools/google_places/__init__.py @@ -0,0 +1 @@ +"""Google Places API Toolkit.""" diff --git a/langchain/tools/google_places/tool.py b/langchain/tools/google_places/tool.py new file mode 100644 index 00000000..31ae39da --- /dev/null +++ b/langchain/tools/google_places/tool.py @@ -0,0 +1,27 @@ +"""Tool for the Google search API.""" + +from pydantic import Field + +from langchain.tools.base import BaseTool +from langchain.utilities.google_places_api import GooglePlacesAPIWrapper + + +class GooglePlacesTool(BaseTool): + """Tool that adds the capability to query the Google places API.""" + + name = "Google Places" + description = ( + "A wrapper around Google Places. " + "Useful for when you need to validate or " + "discover addressed from ambiguous text. " + "Input should be a search query." + ) + api_wrapper: GooglePlacesAPIWrapper = Field(default_factory=GooglePlacesAPIWrapper) + + def _run(self, query: str) -> str: + """Use the tool.""" + return self.api_wrapper.run(query) + + async def _arun(self, query: str) -> str: + """Use the tool asynchronously.""" + raise NotImplementedError("GooglePlacesRun does not support async") diff --git a/langchain/utilities/__init__.py b/langchain/utilities/__init__.py index a41aa842..f834601d 100644 --- a/langchain/utilities/__init__.py +++ b/langchain/utilities/__init__.py @@ -4,6 +4,7 @@ from langchain.utilities.apify import ApifyWrapper from langchain.utilities.arxiv import ArxivAPIWrapper from langchain.utilities.bash import BashProcess from langchain.utilities.bing_search import BingSearchAPIWrapper +from langchain.utilities.google_places_api import GooglePlacesAPIWrapper from langchain.utilities.google_search import GoogleSearchAPIWrapper from langchain.utilities.google_serper import GoogleSerperAPIWrapper from langchain.utilities.openweathermap import OpenWeatherMapAPIWrapper @@ -20,6 +21,7 @@ __all__ = [ "TextRequestsWrapper", "GoogleSearchAPIWrapper", "GoogleSerperAPIWrapper", + "GooglePlacesAPIWrapper", "WolframAlphaAPIWrapper", "SerpAPIWrapper", "SearxSearchWrapper", diff --git a/langchain/utilities/google_places_api.py b/langchain/utilities/google_places_api.py new file mode 100644 index 00000000..585a5242 --- /dev/null +++ b/langchain/utilities/google_places_api.py @@ -0,0 +1,112 @@ +"""Chain that calls Google Places API. +""" + +import logging +from typing import Any, Dict, Optional + +from pydantic import BaseModel, Extra, root_validator + +from langchain.utils import get_from_dict_or_env + + +class GooglePlacesAPIWrapper(BaseModel): + """Wrapper around Google Places API. + + To use, you should have the ``googlemaps`` python package installed, + **an API key for the google maps platform**, + and the enviroment variable ''GPLACES_API_KEY'' + set with your API key , or pass 'gplaces_api_key' + as a named parameter to the constructor. + + By default, this will return the all the results on the input query. + You can use the top_k_results argument to limit the number of results. + + Example: + .. code-block:: python + + + from langchain import GooglePlacesAPIWrapper + gplaceapi = GooglePlacesAPIWrapper() + """ + + gplaces_api_key: Optional[str] = None + google_map_client: Any #: :meta private: + top_k_results: Optional[int] = None + + class Config: + """Configuration for this pydantic object.""" + + extra = Extra.forbid + arbitrary_types_allowed = True + + @root_validator() + def validate_environment(cls, values: Dict) -> Dict: + """Validate that api key is in your environment variable.""" + gplaces_api_key = get_from_dict_or_env( + values, "gplaces_api_key", "GPLACES_API_KEY" + ) + values["gplaces_api_key"] = gplaces_api_key + try: + import googlemaps + + values["google_map_client"] = googlemaps.Client(gplaces_api_key) + except ImportError: + raise ValueError( + "Could not import googlemaps python packge. " + "Please install it with `pip install googlemaps`." + ) + return values + + def run(self, query: str) -> str: + """Run Places search and get k number of places that exists that match.""" + search_results = self.google_map_client.places(query)["results"] + num_to_return = len(search_results) + + places = [] + + if num_to_return == 0: + return "Google Places did not find any places that match the description" + + num_to_return = ( + num_to_return + if self.top_k_results is None + else min(num_to_return, self.top_k_results) + ) + + for i in range(num_to_return): + result = search_results[i] + details = self.fetch_place_details(result["place_id"]) + + if details is not None: + places.append(details) + + return "\n".join([f"{i+1}. {item}" for i, item in enumerate(places)]) + + def fetch_place_details(self, place_id: str) -> Optional[str]: + try: + place_details = self.google_map_client.place(place_id) + formatted_details = self.format_place_details(place_details) + return formatted_details + except Exception as e: + logging.error(f"An Error occurred while fetching place details: {e}") + return None + + def format_place_details(self, place_details: Dict[str, Any]) -> Optional[str]: + try: + name = place_details.get("result", {}).get("name", "Unkown") + address = place_details.get("result", {}).get( + "formatted_address", "Unknown" + ) + phone_number = place_details.get("result", {}).get( + "formatted_phone_number", "Unknown" + ) + website = place_details.get("result", {}).get("website", "Unknown") + + formatted_details = ( + f"{name}\nAddress: {address}\n" + f"Phone: {phone_number}\nWebsite: {website}\n\n" + ) + return formatted_details + except Exception as e: + logging.error(f"An error occurred while formatting place details: {e}") + return None