Source code for camel.toolkits.google_maps_toolkit

# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. ===========
# Licensed under the Apache License, Version 2.0 (the “License”);
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an “AS IS” BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. ===========
import os
from functools import wraps
from typing import Any, Callable, List, Optional, Union

from camel.toolkits.base import BaseToolkit
from camel.toolkits.function_tool import FunctionTool
from camel.utils import dependencies_required


[docs] def handle_googlemaps_exceptions( func: Callable[..., Any], ) -> Callable[..., Any]: r"""Decorator to catch and handle exceptions raised by Google Maps API calls. Args: func (Callable): The function to be wrapped by the decorator. Returns: Callable: A wrapper function that calls the wrapped function and handles exceptions. """ @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: try: # ruff: noqa: E501 from googlemaps.exceptions import ( # type: ignore[import] ApiError, HTTPError, Timeout, TransportError, ) except ImportError: raise ImportError( "Please install `googlemaps` first. You can install " "it by running `pip install googlemaps`." ) try: return func(*args, **kwargs) except ApiError as e: return ( 'An exception returned by the remote API. ' f'Status: {e.status}, Message: {e.message}' ) except HTTPError as e: return ( 'An unexpected HTTP error occurred. ' f'Status Code: {e.status_code}' ) except Timeout: return 'The request timed out.' except TransportError as e: return ( 'Something went wrong while trying to execute the ' f'request. Details: {e.base_exception}' ) except Exception as e: return f'An unexpected error occurred: {e}' return wrapper
def _format_offset_to_natural_language(offset: int) -> str: r"""Converts a time offset in seconds to a more natural language description using hours as the unit, with decimal places to represent minutes and seconds. Args: offset (int): The time offset in seconds. Can be positive, negative, or zero. Returns: str: A string representing the offset in hours, such as "+2.50 hours" or "-3.75 hours". """ # Convert the offset to hours as a float hours = offset / 3600.0 hours_str = f"{hours:+.2f} hour{'s' if abs(hours) != 1 else ''}" return hours_str
[docs] class GoogleMapsToolkit(BaseToolkit): r"""A class representing a toolkit for interacting with GoogleMaps API. This class provides methods for validating addresses, retrieving elevation, and fetching timezone information using the Google Maps API. """ @dependencies_required('googlemaps') def __init__(self) -> None: import googlemaps api_key = os.environ.get('GOOGLE_API_KEY') if not api_key: raise ValueError( "`GOOGLE_API_KEY` not found in environment variables. " "`GOOGLE_API_KEY` API keys are generated in the `Credentials` " "page of the `APIs & Services` tab of " "https://console.cloud.google.com/apis/credentials." ) self.gmaps = googlemaps.Client(key=api_key)
[docs] @handle_googlemaps_exceptions def get_address_description( self, address: Union[str, List[str]], region_code: Optional[str] = None, locality: Optional[str] = None, ) -> str: r"""Validates an address via Google Maps API, returns a descriptive summary. Validates an address using Google Maps API, returning a summary that includes information on address completion, formatted address, location coordinates, and metadata types that are true for the given address. Args: address (Union[str, List[str]]): The address or components to validate. Can be a single string or a list representing different parts. region_code (str, optional): Country code for regional restriction, helps narrow down results. (default: :obj:`None`) locality (str, optional): Restricts validation to a specific locality, e.g., "Mountain View". (default: :obj:`None`) Returns: str: Summary of the address validation results, including information on address completion, formatted address, geographical coordinates (latitude and longitude), and metadata types true for the address. """ addressvalidation_result = self.gmaps.addressvalidation( [address], regionCode=region_code, locality=locality, enableUspsCass=False, ) # Always False as per requirements # Check if the result contains an error if 'error' in addressvalidation_result: error_info = addressvalidation_result['error'] error_message = error_info.get( 'message', 'An unknown error occurred' ) error_status = error_info.get('status', 'UNKNOWN_STATUS') error_code = error_info.get('code', 'UNKNOWN_CODE') return ( f"Address validation failed with error: {error_message} " f"Status: {error_status}, Code: {error_code}" ) # Assuming the successful response structure # includes a 'result' key result = addressvalidation_result['result'] verdict = result.get('verdict', {}) address_info = result.get('address', {}) geocode = result.get('geocode', {}) metadata = result.get('metadata', {}) # Construct the descriptive string address_complete = ( "Yes" if verdict.get('addressComplete', False) else "No" ) formatted_address = address_info.get( 'formattedAddress', 'Not available' ) location = geocode.get('location', {}) latitude = location.get('latitude', 'Not available') longitude = location.get('longitude', 'Not available') true_metadata_types = [key for key, value in metadata.items() if value] true_metadata_types_str = ( ', '.join(true_metadata_types) if true_metadata_types else 'None' ) description = ( f"Address completion status: {address_complete}. " f"Formatted address: {formatted_address}. " f"Location (latitude, longitude): ({latitude}, {longitude}). " f"Metadata indicating true types: {true_metadata_types_str}." ) return description
[docs] @handle_googlemaps_exceptions def get_elevation(self, lat: float, lng: float) -> str: r"""Retrieves elevation data for a given latitude and longitude. Uses the Google Maps API to fetch elevation data for the specified latitude and longitude. It handles exceptions gracefully and returns a description of the elevation, including its value in meters and the data resolution. Args: lat (float): The latitude of the location to query. lng (float): The longitude of the location to query. Returns: str: A description of the elevation at the specified location(s), including the elevation in meters and the data resolution. If elevation data is not available, a message indicating this is returned. """ # Assuming gmaps is a configured Google Maps client instance elevation_result = self.gmaps.elevation((lat, lng)) # Extract the elevation data from the first # (and presumably only) result if elevation_result: elevation = elevation_result[0]['elevation'] location = elevation_result[0]['location'] resolution = elevation_result[0]['resolution'] # Format the elevation data into a natural language description description = ( f"The elevation at latitude {location['lat']}, " f"longitude {location['lng']} " f"is approximately {elevation:.2f} meters above sea level, " f"with a data resolution of {resolution:.2f} meters." ) else: description = ( "Elevation data is not available for the given location." ) return description
[docs] @handle_googlemaps_exceptions def get_timezone(self, lat: float, lng: float) -> str: r"""Retrieves timezone information for a given latitude and longitude. This function uses the Google Maps Timezone API to fetch timezone data for the specified latitude and longitude. It returns a natural language description of the timezone, including the timezone ID, name, standard time offset, daylight saving time offset, and the total offset from Coordinated Universal Time (UTC). Args: lat (float): The latitude of the location to query. lng (float): The longitude of the location to query. Returns: str: A descriptive string of the timezone information, including the timezone ID and name, standard time offset, daylight saving time offset, and total offset from UTC. """ # Get timezone information timezone_dict = self.gmaps.timezone((lat, lng)) # Extract necessary information dst_offset = timezone_dict[ 'dstOffset' ] # Daylight Saving Time offset in seconds raw_offset = timezone_dict[ 'rawOffset' ] # Standard time offset in seconds timezone_id = timezone_dict['timeZoneId'] timezone_name = timezone_dict['timeZoneName'] raw_offset_str = _format_offset_to_natural_language(raw_offset) dst_offset_str = _format_offset_to_natural_language(dst_offset) total_offset_seconds = dst_offset + raw_offset total_offset_str = _format_offset_to_natural_language( total_offset_seconds ) # Create a natural language description description = ( f"Timezone ID is {timezone_id}, named {timezone_name}. " f"The standard time offset is {raw_offset_str}. " f"Daylight Saving Time offset is {dst_offset_str}. " f"The total offset from Coordinated Universal Time (UTC) is " f"{total_offset_str}, including any Daylight Saving Time " f"adjustment if applicable. " ) return description
[docs] def get_tools(self) -> List[FunctionTool]: r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ return [ FunctionTool(self.get_address_description), FunctionTool(self.get_elevation), FunctionTool(self.get_timezone), ]