File size: 11,948 Bytes
62da328 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# ========= Copyright 2023-2024 @ 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-2024 @ 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
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
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)
@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
@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
@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
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),
]
|