Spaces:
Sleeping
Sleeping
File size: 7,049 Bytes
f64aa7a 3fd71f2 7d0a287 3fd71f2 7718004 80f8dff f64aa7a 3fd71f2 c81eeba 3fd71f2 c81eeba 3fd71f2 c81eeba 3fd71f2 c81eeba 3fd71f2 c81eeba 3fd71f2 c81eeba 3fd71f2 c81eeba 3fd71f2 c81eeba 3fd71f2 7d0a287 f64aa7a 3fd71f2 f64aa7a 3fd71f2 f64aa7a 3fd71f2 f64aa7a 3fd71f2 f64aa7a 3fd71f2 f64aa7a 3fd71f2 f64aa7a 3fd71f2 c81eeba f64aa7a 3fd71f2 f64aa7a 3fd71f2 7718004 f64aa7a 7718004 3fd71f2 f64aa7a 3fd71f2 c81eeba f64aa7a 7718004 3fd71f2 | 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 | import os
import httpx
from typing import List, Dict, Optional, Any, Union
from crewai.tools import BaseTool
from pydantic import BaseModel, Field, ConfigDict, field_validator
from utils.cache import SQLiteCache
cache = SQLiteCache("/tmp/cache.sqlite")
class GoogleMapsToolSchema(BaseModel):
model_config = ConfigDict(extra="ignore")
location: str
activities: Optional[Union[List[str], str]] = None
cuisine_preferences: Optional[Union[Dict[str, str], List[str], str]] = None
max_results_per_query: int = Field(default=20, ge=1, le=50)
@field_validator("location", mode="before")
@classmethod
def _norm_location(cls, v: Any) -> str:
if isinstance(v, dict):
v = v.get("location") or v.get("value") or v.get("description") or ""
return str(v).strip()
@field_validator("activities", mode="before")
@classmethod
def _norm_activities(cls, v: Any):
if v is None:
return None
if isinstance(v, dict):
v = v.get("activities") or v.get("value") or v.get("description") or v
if isinstance(v, str):
# allow "art, craft beer, nightlife"
items = [s.strip() for s in v.split(",")]
return [s for s in items if s]
if isinstance(v, list):
out: List[str] = []
for x in v:
if x is None:
continue
out.append(str(x).strip())
return [s for s in out if s]
return None
@field_validator("cuisine_preferences", mode="before")
@classmethod
def _norm_cuisine(cls, v: Any):
"""
Accepts:
- {"breakfast":"pastry","lunch":"seafood",...}
- {"breakfast": ["vegan", "local"], "lunch":[],...}
- "bolognese"
- ["bolognese"]
Normalizes to dict[str, str] or None.
"""
if v is None:
return None
def norm_val(val: Any) -> str:
if val is None:
return ""
if isinstance(val, list):
parts = [str(x).strip() for x in val if str(x).strip()]
return ", ".join(parts)
return str(val).strip()
if isinstance(v, dict):
out = {}
# normalize keys to lowercase
for k, val in v.items():
key = str(k).strip().lower()
s = norm_val(val)
if s:
out[key] = s
return out or None
if isinstance(v, list):
# treat first item as a global cuisine hint
s = norm_val(v)
return {"breakfast": s, "lunch": s, "dinner": s} if s else None
if isinstance(v, str):
s = v.strip()
return {"breakfast": s, "lunch": s, "dinner": s} if s else None
return None
class GoogleMapsTool(BaseTool):
"""
CrewAI compatible tool for querying Google Places Text Search API.
Preference-driven: if you pass activities extracted from user preferences,
it will search those terms too (without hardcoding bar/craft beer logic).
"""
name: str = "Google Maps Places Tool"
description: str = (
"Searches for places of interest in a city using Google Places Text Search. "
"Returns categorized lists (meals + base activities + optional preference activities)."
)
args_schema = GoogleMapsToolSchema
def _run(
self,
location: str,
activities: Optional[Union[List[str], str]] = None,
cuisine_preferences: Optional[Union[Dict[str, str], List[str], str]] = None,
max_results_per_query: int = 20,
) -> Dict[str, List[Dict]]:
api_key = os.getenv("GOOGLE_MAPS_API_KEY")
if not api_key:
raise ValueError("Missing GOOGLE_MAPS_API_KEY in environment variables")
base_url = "https://maps.googleapis.com/maps/api/place/textsearch/json"
meal_categories = ["breakfast", "lunch", "dinner"]
base_activity_categories = ["museums", "parks", "landmarks"]
# normalize inputs (in case tool is called directly without Pydantic)
if isinstance(activities, str):
activities = [s.strip() for s in activities.split(",") if s.strip()]
if cuisine_preferences and not isinstance(cuisine_preferences, dict):
# let schema handle normally; but keep a fallback
s = str(cuisine_preferences).strip()
cuisine_preferences = {"breakfast": s, "lunch": s, "dinner": s} if s else None
extra_activities = activities or []
# ✅ keep defaults AND add preference-driven extras
categories: List[str] = []
for c in (meal_categories + base_activity_categories + list(extra_activities)):
c = str(c).strip()
if c and c not in categories:
categories.append(c)
all_results: Dict[str, List[Dict]] = {}
with httpx.Client(timeout=15.0) as client:
for category in categories:
if category in meal_categories:
hint = (cuisine_preferences or {}).get(category)
if hint:
query = f"{hint} {category} in {location}"
else:
query = f"{category} restaurants in {location}"
else:
# preference-driven term, no hardcoded “bar/craft beer” expansions
query = f"{category} in {location}"
# cache by query (safer than category-only)
qk = query.strip().lower()
cache_key = f"places::q::{qk}"
cached = cache.get(cache_key)
if cached is not None:
all_results[category] = cached
continue
params = {"query": query, "key": api_key}
resp = client.get(base_url, params=params)
resp.raise_for_status()
data = resp.json()
places = [
{
"name": r.get("name"),
"category": category,
"rating": r.get("rating"),
"address": r.get("formatted_address"),
"lat": r.get("geometry", {}).get("location", {}).get("lat"),
"lng": r.get("geometry", {}).get("location", {}).get("lng"),
"user_ratings_total": r.get("user_ratings_total"),
"place_id": r.get("place_id"),
"types": r.get("types", []),
"price_level": r.get("price_level"),
"business_status": r.get("business_status"),
}
for r in data.get("results", [])[:max_results_per_query]
]
all_results[category] = places
cache.set(cache_key, places, ttl_seconds=7 * 24 * 3600)
return all_results |