| """ |
| Google Maps Platform API Tool for EvoAgentX |
| |
| This module provides comprehensive Google Maps Platform integration including: |
| - Geocoding API: Convert addresses to coordinates and vice versa |
| - Places API: Search for places and get detailed information |
| - Routes API: Calculate directions and distance matrices |
| - Time Zone API: Get time zone information for locations |
| |
| Compatible with EvoAgentX tool architecture and follows the latest Google Maps Platform APIs. |
| """ |
|
|
| import requests |
| import json |
| import os |
| from typing import Dict, Any, List |
|
|
| from .tool import Tool, Toolkit |
| from ..core.module import BaseModule |
| from ..core.logging import logger |
|
|
|
|
| class GoogleMapsBase(BaseModule): |
| """ |
| Base class for Google Maps Platform API interactions. |
| Handles API key management, request formatting, and common utilities. |
| """ |
| |
| def __init__(self, api_key: str = None, timeout: int = 10, **kwargs): |
| """ |
| Initialize the Google Maps base. |
| |
| Args: |
| api_key (str, optional): Google Maps Platform API key. If not provided, will try to get from GOOGLE_MAPS_API_KEY environment variable. |
| timeout (int): Request timeout in seconds |
| **kwargs: Additional keyword arguments for parent class |
| """ |
| super().__init__(**kwargs) |
| |
| |
| self.api_key = api_key or os.getenv("GOOGLE_MAPS_API_KEY") |
| |
| if not self.api_key: |
| logger.warning( |
| "No Google Maps API key provided. Please set GOOGLE_MAPS_API_KEY environment variable " |
| "or pass api_key parameter. Get your API key from: https://console.cloud.google.com/apis/" |
| ) |
| |
| self.timeout = timeout |
| self.base_url = "https://maps.googleapis.com/maps/api" |
| |
| def _make_request(self, endpoint: str, params: Dict[str, Any]) -> Dict[str, Any]: |
| """ |
| Make a request to Google Maps Platform API. |
| |
| Args: |
| endpoint (str): API endpoint |
| params (dict): Request parameters |
| |
| Returns: |
| dict: API response |
| """ |
| |
| if not self.api_key: |
| return { |
| "success": False, |
| "error": "Google Maps API key not found. Please set GOOGLE_MAPS_API_KEY environment variable or pass api_key parameter." |
| } |
| |
| try: |
| |
| params['key'] = self.api_key |
| |
| |
| url = f"{self.base_url}/{endpoint}" |
| |
| |
| response = requests.get(url, params=params, timeout=self.timeout) |
| response.raise_for_status() |
| |
| |
| data = response.json() |
| |
| |
| status = data.get('status', 'UNKNOWN_ERROR') |
| if status == 'OK': |
| return { |
| "success": True, |
| "status": status, |
| "data": data |
| } |
| elif status == 'ZERO_RESULTS': |
| return { |
| "success": True, |
| "status": status, |
| "data": data, |
| "message": "No results found" |
| } |
| else: |
| error_message = data.get('error_message', f"API returned status: {status}") |
| logger.error(f"Google Maps API error: {error_message}") |
| return { |
| "success": False, |
| "status": status, |
| "error": error_message |
| } |
| |
| except requests.exceptions.RequestException as e: |
| logger.error(f"Request error: {str(e)}") |
| return { |
| "success": False, |
| "error": f"Request failed: {str(e)}" |
| } |
| except json.JSONDecodeError as e: |
| logger.error(f"JSON decode error: {str(e)}") |
| return { |
| "success": False, |
| "error": f"Invalid JSON response: {str(e)}" |
| } |
| except Exception as e: |
| logger.error(f"Unexpected error: {str(e)}") |
| return { |
| "success": False, |
| "error": f"Unexpected error: {str(e)}" |
| } |
|
|
| def _format_coordinates(self, lat: float, lng: float) -> str: |
| """Format coordinates for API requests.""" |
| return f"{lat},{lng}" |
|
|
|
|
| class GeocodeAddressTool(Tool): |
| """Convert addresses to geographic coordinates (latitude/longitude).""" |
| |
| name: str = "geocode_address" |
| description: str = "Convert a street address into geographic coordinates (latitude and longitude). Useful for finding exact locations of places." |
| inputs: Dict[str, Dict[str, str]] = { |
| "address": { |
| "type": "string", |
| "description": "The street address to geocode (e.g., '1600 Amphitheatre Parkway, Mountain View, CA')" |
| }, |
| "components": { |
| "type": "string", |
| "description": "Optional component filters (e.g., 'country:US|locality:Mountain View')" |
| }, |
| "region": { |
| "type": "string", |
| "description": "Optional region code for biasing results (e.g., 'us', 'uk')" |
| } |
| } |
| required: List[str] = ["address"] |
| |
| def __init__(self, google_maps_base: GoogleMapsBase): |
| super().__init__() |
| self.google_maps_base = google_maps_base |
| |
| def __call__(self, address: str, components: str = None, region: str = None) -> Dict[str, Any]: |
| """ |
| Geocode an address to coordinates. |
| |
| Args: |
| address: Street address to geocode |
| components: Optional component filters |
| region: Optional region bias |
| |
| Returns: |
| Dictionary with geocoding results |
| """ |
| params = {"address": address} |
| |
| if components: |
| params["components"] = components |
| if region: |
| params["region"] = region |
| |
| result = self.google_maps_base._make_request("geocode/json", params) |
| |
| if result["success"] and result["data"].get("results"): |
| |
| geocode_result = result["data"]["results"][0] |
| location = geocode_result["geometry"]["location"] |
| |
| return { |
| "success": True, |
| "address": address, |
| "formatted_address": geocode_result.get("formatted_address"), |
| "latitude": location["lat"], |
| "longitude": location["lng"], |
| "place_id": geocode_result.get("place_id"), |
| "location_type": geocode_result["geometry"].get("location_type"), |
| "address_components": geocode_result.get("address_components", []) |
| } |
| else: |
| return { |
| "success": False, |
| "address": address, |
| "error": result.get("error", "No results found") |
| } |
|
|
|
|
| class ReverseGeocodeTool(Tool): |
| """Convert geographic coordinates to a human-readable address.""" |
| |
| name: str = "reverse_geocode" |
| description: str = "Convert geographic coordinates (latitude and longitude) into a human-readable address." |
| inputs: Dict[str, Dict[str, str]] = { |
| "latitude": { |
| "type": "number", |
| "description": "Latitude coordinate" |
| }, |
| "longitude": { |
| "type": "number", |
| "description": "Longitude coordinate" |
| }, |
| "result_type": { |
| "type": "string", |
| "description": "Optional filter for result types (e.g., 'street_address|route')" |
| } |
| } |
| required: List[str] = ["latitude", "longitude"] |
| |
| def __init__(self, google_maps_base: GoogleMapsBase): |
| super().__init__() |
| self.google_maps_base = google_maps_base |
| |
| def __call__(self, latitude: float, longitude: float, result_type: str = None) -> Dict[str, Any]: |
| """ |
| Reverse geocode coordinates to address. |
| |
| Args: |
| latitude: Latitude coordinate |
| longitude: Longitude coordinate |
| result_type: Optional result type filter |
| |
| Returns: |
| Dictionary with reverse geocoding results |
| """ |
| latlng = self.google_maps_base._format_coordinates(latitude, longitude) |
| params = {"latlng": latlng} |
| |
| if result_type: |
| params["result_type"] = result_type |
| |
| result = self.google_maps_base._make_request("geocode/json", params) |
| |
| if result["success"] and result["data"].get("results"): |
| addresses = [] |
| for geocode_result in result["data"]["results"]: |
| addresses.append({ |
| "formatted_address": geocode_result.get("formatted_address"), |
| "place_id": geocode_result.get("place_id"), |
| "types": geocode_result.get("types", []), |
| "address_components": geocode_result.get("address_components", []) |
| }) |
| |
| return { |
| "success": True, |
| "latitude": latitude, |
| "longitude": longitude, |
| "addresses": addresses |
| } |
| else: |
| return { |
| "success": False, |
| "latitude": latitude, |
| "longitude": longitude, |
| "error": result.get("error", "No results found") |
| } |
|
|
|
|
| class PlacesSearchTool(Tool): |
| """Search for places using text queries or nearby location.""" |
| |
| name: str = "places_search" |
| description: str = "Search for places (restaurants, shops, landmarks) using text queries. Can search near a specific location." |
| inputs: Dict[str, Dict[str, str]] = { |
| "query": { |
| "type": "string", |
| "description": "Text search query (e.g., 'pizza restaurants near Times Square')" |
| }, |
| "location": { |
| "type": "string", |
| "description": "Optional location bias as 'latitude,longitude' (e.g., '40.7589,-73.9851')" |
| }, |
| "radius": { |
| "type": "number", |
| "description": "Optional search radius in meters (max 50000)" |
| }, |
| "type": { |
| "type": "string", |
| "description": "Optional place type filter (e.g., 'restaurant', 'gas_station')" |
| } |
| } |
| required: List[str] = ["query"] |
| |
| def __init__(self, google_maps_base: GoogleMapsBase): |
| super().__init__() |
| self.google_maps_base = google_maps_base |
| |
| def __call__(self, query: str, location: str = None, radius: float = None, type: str = None) -> Dict[str, Any]: |
| """ |
| Search for places using text query. |
| |
| Args: |
| query: Text search query |
| location: Optional location bias as 'lat,lng' |
| radius: Optional search radius in meters |
| type: Optional place type filter |
| |
| Returns: |
| Dictionary with search results |
| """ |
| params = {"query": query} |
| |
| if location: |
| params["location"] = location |
| if radius: |
| params["radius"] = min(radius, 50000) |
| if type: |
| params["type"] = type |
| |
| result = self.google_maps_base._make_request("place/textsearch/json", params) |
| |
| if result["success"]: |
| places = [] |
| for place in result["data"].get("results", []): |
| places.append({ |
| "name": place.get("name"), |
| "place_id": place.get("place_id"), |
| "formatted_address": place.get("formatted_address"), |
| "rating": place.get("rating"), |
| "user_ratings_total": place.get("user_ratings_total"), |
| "price_level": place.get("price_level"), |
| "types": place.get("types", []), |
| "geometry": place.get("geometry", {}), |
| "business_status": place.get("business_status") |
| }) |
| |
| return { |
| "success": True, |
| "query": query, |
| "places_found": len(places), |
| "places": places |
| } |
| else: |
| return { |
| "success": False, |
| "query": query, |
| "error": result.get("error", "Search failed") |
| } |
|
|
|
|
| class PlaceDetailsTool(Tool): |
| """Get detailed information about a specific place using its Place ID.""" |
| |
| name: str = "place_details" |
| description: str = "Get comprehensive information about a specific place using its Place ID, including contact info, hours, reviews." |
| inputs: Dict[str, Dict[str, str]] = { |
| "place_id": { |
| "type": "string", |
| "description": "Unique Place ID from a place search" |
| }, |
| "fields": { |
| "type": "string", |
| "description": "Optional comma-separated list of fields to return (e.g., 'name,rating,formatted_phone_number')" |
| } |
| } |
| required: List[str] = ["place_id"] |
| |
| def __init__(self, google_maps_base: GoogleMapsBase): |
| super().__init__() |
| self.google_maps_base = google_maps_base |
| |
| def __call__(self, place_id: str, fields: str = None) -> Dict[str, Any]: |
| """ |
| Get detailed place information. |
| |
| Args: |
| place_id: Unique place identifier |
| fields: Optional fields to return |
| |
| Returns: |
| Dictionary with place details |
| """ |
| params = {"place_id": place_id} |
| |
| |
| if not fields: |
| fields = "name,formatted_address,formatted_phone_number,website,rating,user_ratings_total,opening_hours,price_level,types,geometry" |
| |
| params["fields"] = fields |
| |
| result = self.google_maps_base._make_request("place/details/json", params) |
| |
| if result["success"] and result["data"].get("result"): |
| place = result["data"]["result"] |
| |
| return { |
| "success": True, |
| "place_id": place_id, |
| "name": place.get("name"), |
| "formatted_address": place.get("formatted_address"), |
| "phone_number": place.get("formatted_phone_number"), |
| "international_phone": place.get("international_phone_number"), |
| "website": place.get("website"), |
| "rating": place.get("rating"), |
| "user_ratings_total": place.get("user_ratings_total"), |
| "price_level": place.get("price_level"), |
| "types": place.get("types", []), |
| "opening_hours": place.get("opening_hours"), |
| "geometry": place.get("geometry", {}), |
| "business_status": place.get("business_status"), |
| "reviews": place.get("reviews", []) |
| } |
| else: |
| return { |
| "success": False, |
| "place_id": place_id, |
| "error": result.get("error", "Place not found") |
| } |
|
|
|
|
| class DirectionsTool(Tool): |
| """Calculate driving, walking, bicycling, or transit directions between locations.""" |
| |
| name: str = "directions" |
| description: str = "Calculate directions between two or more locations with different travel modes (driving, walking, bicycling, transit)." |
| inputs: Dict[str, Dict[str, str]] = { |
| "origin": { |
| "type": "string", |
| "description": "Starting location (address, coordinates, or place ID)" |
| }, |
| "destination": { |
| "type": "string", |
| "description": "Ending location (address, coordinates, or place ID)" |
| }, |
| "mode": { |
| "type": "string", |
| "description": "Travel mode: 'driving', 'walking', 'bicycling', or 'transit' (default: driving)" |
| }, |
| "waypoints": { |
| "type": "string", |
| "description": "Optional waypoints separated by '|' (e.g., 'via:San Francisco|via:Los Angeles')" |
| }, |
| "alternatives": { |
| "type": "boolean", |
| "description": "Whether to return alternative routes (default: false)" |
| } |
| } |
| required: List[str] = ["origin", "destination"] |
| |
| def __init__(self, google_maps_base: GoogleMapsBase): |
| super().__init__() |
| self.google_maps_base = google_maps_base |
| |
| def __call__(self, origin: str, destination: str, mode: str = "driving", |
| waypoints: str = None, alternatives: bool = False) -> Dict[str, Any]: |
| """ |
| Calculate directions between locations. |
| |
| Args: |
| origin: Starting location |
| destination: Ending location |
| mode: Travel mode |
| waypoints: Optional waypoints |
| alternatives: Return alternative routes |
| |
| Returns: |
| Dictionary with directions |
| """ |
| params = { |
| "origin": origin, |
| "destination": destination, |
| "mode": mode, |
| "alternatives": alternatives |
| } |
| |
| if waypoints: |
| params["waypoints"] = waypoints |
| |
| result = self.google_maps_base._make_request("directions/json", params) |
| |
| if result["success"] and result["data"].get("routes"): |
| routes = [] |
| for route in result["data"]["routes"]: |
| |
| legs = [] |
| total_distance = 0 |
| total_duration = 0 |
| |
| for leg in route.get("legs", []): |
| leg_info = { |
| "start_address": leg.get("start_address"), |
| "end_address": leg.get("end_address"), |
| "distance": leg.get("distance", {}), |
| "duration": leg.get("duration", {}), |
| "steps": [] |
| } |
| |
| |
| if leg.get("distance", {}).get("value"): |
| total_distance += leg["distance"]["value"] |
| if leg.get("duration", {}).get("value"): |
| total_duration += leg["duration"]["value"] |
| |
| |
| for step in leg.get("steps", []): |
| leg_info["steps"].append({ |
| "instructions": step.get("html_instructions", ""), |
| "distance": step.get("distance", {}), |
| "duration": step.get("duration", {}), |
| "travel_mode": step.get("travel_mode") |
| }) |
| |
| legs.append(leg_info) |
| |
| routes.append({ |
| "summary": route.get("summary"), |
| "legs": legs, |
| "total_distance_meters": total_distance, |
| "total_duration_seconds": total_duration, |
| "overview_polyline": route.get("overview_polyline", {}), |
| "warnings": route.get("warnings", []), |
| "copyrights": route.get("copyrights") |
| }) |
| |
| return { |
| "success": True, |
| "origin": origin, |
| "destination": destination, |
| "mode": mode, |
| "routes": routes |
| } |
| else: |
| return { |
| "success": False, |
| "origin": origin, |
| "destination": destination, |
| "error": result.get("error", "No routes found") |
| } |
|
|
|
|
| class DistanceMatrixTool(Tool): |
| """Calculate travel times and distances between multiple origins and destinations.""" |
| |
| name: str = "distance_matrix" |
| description: str = "Calculate travel times and distances between multiple origins and destinations. Useful for finding the closest location." |
| inputs: Dict[str, Dict[str, str]] = { |
| "origins": { |
| "type": "string", |
| "description": "Origin locations separated by '|' (e.g., 'Seattle,WA|Portland,OR')" |
| }, |
| "destinations": { |
| "type": "string", |
| "description": "Destination locations separated by '|' (e.g., 'San Francisco,CA|Los Angeles,CA')" |
| }, |
| "mode": { |
| "type": "string", |
| "description": "Travel mode: 'driving', 'walking', 'bicycling', or 'transit' (default: driving)" |
| }, |
| "units": { |
| "type": "string", |
| "description": "Unit system: 'metric' or 'imperial' (default: metric)" |
| } |
| } |
| required: List[str] = ["origins", "destinations"] |
| |
| def __init__(self, google_maps_base: GoogleMapsBase): |
| super().__init__() |
| self.google_maps_base = google_maps_base |
| |
| def __call__(self, origins: str, destinations: str, mode: str = "driving", |
| units: str = "metric") -> Dict[str, Any]: |
| """ |
| Calculate distance matrix. |
| |
| Args: |
| origins: Origin locations separated by '|' |
| destinations: Destination locations separated by '|' |
| mode: Travel mode |
| units: Unit system |
| |
| Returns: |
| Dictionary with distance matrix |
| """ |
| params = { |
| "origins": origins, |
| "destinations": destinations, |
| "mode": mode, |
| "units": units |
| } |
| |
| result = self.google_maps_base._make_request("distancematrix/json", params) |
| |
| if result["success"] and result["data"].get("rows"): |
| origin_addresses = result["data"].get("origin_addresses", []) |
| destination_addresses = result["data"].get("destination_addresses", []) |
| |
| matrix = [] |
| for i, row in enumerate(result["data"]["rows"]): |
| origin_results = { |
| "origin_address": origin_addresses[i] if i < len(origin_addresses) else f"Origin {i+1}", |
| "destinations": [] |
| } |
| |
| for j, element in enumerate(row.get("elements", [])): |
| destination_result = { |
| "destination_address": destination_addresses[j] if j < len(destination_addresses) else f"Destination {j+1}", |
| "status": element.get("status"), |
| "distance": element.get("distance", {}), |
| "duration": element.get("duration", {}), |
| "duration_in_traffic": element.get("duration_in_traffic", {}) |
| } |
| origin_results["destinations"].append(destination_result) |
| |
| matrix.append(origin_results) |
| |
| return { |
| "success": True, |
| "origins": origins.split("|"), |
| "destinations": destinations.split("|"), |
| "mode": mode, |
| "units": units, |
| "matrix": matrix |
| } |
| else: |
| return { |
| "success": False, |
| "origins": origins, |
| "destinations": destinations, |
| "error": result.get("error", "Distance matrix calculation failed") |
| } |
|
|
|
|
| class TimeZoneTool(Tool): |
| """Get time zone information for a location.""" |
| |
| name: str = "timezone" |
| description: str = "Get time zone information for a specific location using coordinates." |
| inputs: Dict[str, Dict[str, str]] = { |
| "latitude": { |
| "type": "number", |
| "description": "Latitude coordinate" |
| }, |
| "longitude": { |
| "type": "number", |
| "description": "Longitude coordinate" |
| }, |
| "timestamp": { |
| "type": "number", |
| "description": "Optional Unix timestamp for the desired time (default: current time)" |
| } |
| } |
| required: List[str] = ["latitude", "longitude"] |
| |
| def __init__(self, google_maps_base: GoogleMapsBase): |
| super().__init__() |
| self.google_maps_base = google_maps_base |
| |
| def __call__(self, latitude: float, longitude: float, timestamp: float = None) -> Dict[str, Any]: |
| """ |
| Get time zone information. |
| |
| Args: |
| latitude: Latitude coordinate |
| longitude: Longitude coordinate |
| timestamp: Optional Unix timestamp |
| |
| Returns: |
| Dictionary with time zone info |
| """ |
| import time |
| |
| location = self.google_maps_base._format_coordinates(latitude, longitude) |
| params = { |
| "location": location, |
| "timestamp": timestamp or int(time.time()) |
| } |
| |
| result = self.google_maps_base._make_request("timezone/json", params) |
| |
| if result["success"]: |
| data = result["data"] |
| return { |
| "success": True, |
| "latitude": latitude, |
| "longitude": longitude, |
| "time_zone_id": data.get("timeZoneId"), |
| "time_zone_name": data.get("timeZoneName"), |
| "dst_offset": data.get("dstOffset"), |
| "raw_offset": data.get("rawOffset"), |
| "status": data.get("status") |
| } |
| else: |
| return { |
| "success": False, |
| "latitude": latitude, |
| "longitude": longitude, |
| "error": result.get("error", "Time zone lookup failed") |
| } |
|
|
|
|
| class GoogleMapsToolkit(Toolkit): |
| """ |
| Complete Google Maps Platform toolkit containing all available tools. |
| """ |
| |
| def __init__(self, api_key: str = None, timeout: int = 10, name: str = "GoogleMapsToolkit"): |
| """ |
| Initialize the Google Maps toolkit. |
| |
| Args: |
| api_key (str, optional): Google Maps Platform API key. If not provided, will try to get from GOOGLE_MAPS_API_KEY environment variable. |
| timeout (int): Request timeout in seconds |
| name (str): Toolkit name |
| """ |
| |
| google_maps_base = GoogleMapsBase(api_key=api_key, timeout=timeout) |
| |
| |
| tools = [ |
| GeocodeAddressTool(google_maps_base=google_maps_base), |
| ReverseGeocodeTool(google_maps_base=google_maps_base), |
| PlacesSearchTool(google_maps_base=google_maps_base), |
| PlaceDetailsTool(google_maps_base=google_maps_base), |
| DirectionsTool(google_maps_base=google_maps_base), |
| DistanceMatrixTool(google_maps_base=google_maps_base), |
| TimeZoneTool(google_maps_base=google_maps_base) |
| ] |
| |
| |
| super().__init__(name=name, tools=tools) |
| |
| |
| self.google_maps_base = google_maps_base |
|
|