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),
        ]