mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(google-maps): validate integration against live API docs, add Places Nearby Search (#5484)
* fix(google-maps): validate integration against live API docs, add Places Nearby Search - add google_maps_places_nearby tool (Places API New, searchNearby) for radius/type-based place discovery - add pageToken support to places_search (text search pagination) - add units param to speed_limits (Roads API KPH/MPH) - wire homeMobileCountryCode/homeMobileNetworkCode subblocks for geolocate - split radius subblock so places_nearby's required radius isn't hidden in advanced mode - add default value to rankPreference dropdown - add missing authMode: AuthMode.ApiKey on the block * fix(google-maps): mark elevation resolution optional per API docs Google's Elevation API omits resolution when it can't be determined, but our output schema declared it as a required number. Also fixes a stale comment calling Speed Limits "deprecated" when it's actually Asset Tracking-license restricted. * fix(google-maps): guard radius NaN parse, surface pageToken delay caveat - radius parsing in transformParams now guards NaN like every other numeric param in this block, so a non-numeric radius no longer silently sends "radius":null to the required Nearby Search field - pageToken description/placeholder now note the required delay before a token becomes valid, per Places Text Search API behavior
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { GoogleMapsIcon } from '@/components/icons'
|
||||
import type { BlockConfig, BlockMeta } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
|
||||
export const GoogleMapsBlock: BlockConfig = {
|
||||
type: 'google_maps',
|
||||
@@ -11,6 +11,7 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
docsLink: 'https://docs.sim.ai/integrations/google_maps',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Search,
|
||||
authMode: AuthMode.ApiKey,
|
||||
bgColor: '#FFFFFF',
|
||||
icon: GoogleMapsIcon,
|
||||
|
||||
@@ -26,6 +27,7 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
{ label: 'Get Directions', id: 'directions' },
|
||||
{ label: 'Distance Matrix', id: 'distance_matrix' },
|
||||
{ label: 'Search Places', id: 'places_search' },
|
||||
{ label: 'Nearby Places', id: 'places_nearby' },
|
||||
{ label: 'Place Details', id: 'place_details' },
|
||||
{ label: 'Get Elevation', id: 'elevation' },
|
||||
{ label: 'Get Timezone', id: 'timezone' },
|
||||
@@ -51,7 +53,7 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
hideWhenHosted: true,
|
||||
condition: { field: 'operation', value: 'speed_limits', not: true },
|
||||
},
|
||||
// API Key — always visible for Speed Limits (deprecated API, no hosted key support)
|
||||
// API Key — always visible for Speed Limits (Asset Tracking-license restricted, no hosted key support)
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
@@ -196,6 +198,15 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
condition: { field: 'operation', value: 'places_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Radius — required and always visible for Nearby Places (mandatory search area)
|
||||
{
|
||||
id: 'radius',
|
||||
title: 'Radius (meters)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search radius in meters, up to 50000 (e.g., 5000)',
|
||||
condition: { field: 'operation', value: 'places_nearby' },
|
||||
required: { field: 'operation', value: 'places_nearby' },
|
||||
},
|
||||
{
|
||||
id: 'placeType',
|
||||
title: 'Place Type',
|
||||
@@ -222,9 +233,37 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
{ label: 'Bus Station', id: 'bus_station' },
|
||||
{ label: 'Parking', id: 'parking' },
|
||||
],
|
||||
condition: { field: 'operation', value: ['places_search', 'places_nearby'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'pageToken',
|
||||
title: 'Page Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Token from a previous search (wait ~2s before using it)',
|
||||
condition: { field: 'operation', value: 'places_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'maxResultCount',
|
||||
title: 'Max Results',
|
||||
type: 'short-input',
|
||||
placeholder: 'Maximum number of results (1-20, defaults to 20)',
|
||||
condition: { field: 'operation', value: 'places_nearby' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'rankPreference',
|
||||
title: 'Rank By',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Popularity', id: 'POPULARITY' },
|
||||
{ label: 'Distance', id: 'DISTANCE' },
|
||||
],
|
||||
value: () => 'POPULARITY',
|
||||
condition: { field: 'operation', value: 'places_nearby' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'placeId',
|
||||
@@ -269,6 +308,18 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
rows: 2,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'speedUnits',
|
||||
title: 'Speed Units',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'KPH', id: 'KPH' },
|
||||
{ label: 'MPH', id: 'MPH' },
|
||||
],
|
||||
value: () => 'KPH',
|
||||
condition: { field: 'operation', value: 'speed_limits' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'addressToValidate',
|
||||
@@ -284,7 +335,7 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
title: 'Region Code',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO country code (e.g., US, CA, GB)',
|
||||
condition: { field: 'operation', value: 'validate_address' },
|
||||
condition: { field: 'operation', value: ['validate_address', 'places_nearby'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
@@ -332,6 +383,22 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
condition: { field: 'operation', value: 'geolocate' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'homeMobileCountryCode',
|
||||
title: 'Home Mobile Country Code',
|
||||
type: 'short-input',
|
||||
placeholder: 'Home network MCC (e.g., 310)',
|
||||
condition: { field: 'operation', value: 'geolocate' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'homeMobileNetworkCode',
|
||||
title: 'Home Mobile Network Code',
|
||||
type: 'short-input',
|
||||
placeholder: 'Home network MNC (e.g., 410)',
|
||||
condition: { field: 'operation', value: 'geolocate' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'wifiAccessPoints',
|
||||
title: 'WiFi Access Points',
|
||||
@@ -356,23 +423,23 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
title: 'Latitude',
|
||||
type: 'short-input',
|
||||
placeholder: '37.4224764',
|
||||
condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] },
|
||||
required: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] },
|
||||
condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar', 'places_nearby'] },
|
||||
required: { field: 'operation', value: ['air_quality', 'pollen', 'solar', 'places_nearby'] },
|
||||
},
|
||||
{
|
||||
id: 'aqLongitude',
|
||||
title: 'Longitude',
|
||||
type: 'short-input',
|
||||
placeholder: '-122.0842499',
|
||||
condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] },
|
||||
required: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] },
|
||||
condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar', 'places_nearby'] },
|
||||
required: { field: 'operation', value: ['air_quality', 'pollen', 'solar', 'places_nearby'] },
|
||||
},
|
||||
{
|
||||
id: 'languageCode',
|
||||
title: 'Language Code',
|
||||
type: 'short-input',
|
||||
placeholder: 'Language code (e.g., en, es)',
|
||||
condition: { field: 'operation', value: ['air_quality', 'pollen'] },
|
||||
condition: { field: 'operation', value: ['air_quality', 'pollen', 'places_nearby'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
@@ -412,6 +479,18 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
type: 'short-input',
|
||||
placeholder: 'Language code (e.g., en, es, fr, de)',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'geocode',
|
||||
'reverse_geocode',
|
||||
'directions',
|
||||
'distance_matrix',
|
||||
'places_search',
|
||||
'place_details',
|
||||
'timezone',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'region',
|
||||
@@ -432,6 +511,7 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
'google_maps_geocode',
|
||||
'google_maps_geolocate',
|
||||
'google_maps_place_details',
|
||||
'google_maps_places_nearby',
|
||||
'google_maps_places_search',
|
||||
'google_maps_pollen',
|
||||
'google_maps_reverse_geocode',
|
||||
@@ -490,7 +570,8 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
|
||||
let radius: number | undefined
|
||||
if (params.radius) {
|
||||
radius = Number.parseInt(params.radius, 10)
|
||||
const parsedRadius = Number.parseInt(params.radius, 10)
|
||||
radius = Number.isNaN(parsedRadius) ? undefined : parsedRadius
|
||||
}
|
||||
|
||||
let placeIds: string[] | undefined
|
||||
@@ -549,6 +630,24 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
params.plantsDescription === 'true' || params.plantsDescription === true
|
||||
}
|
||||
|
||||
let maxResultCount: number | undefined
|
||||
if (params.maxResultCount) {
|
||||
const parsedMaxResultCount = Number.parseInt(params.maxResultCount, 10)
|
||||
maxResultCount = Number.isNaN(parsedMaxResultCount) ? undefined : parsedMaxResultCount
|
||||
}
|
||||
|
||||
let homeMobileCountryCode: number | undefined
|
||||
if (params.homeMobileCountryCode) {
|
||||
const parsed = Number.parseInt(params.homeMobileCountryCode, 10)
|
||||
homeMobileCountryCode = Number.isNaN(parsed) ? undefined : parsed
|
||||
}
|
||||
|
||||
let homeMobileNetworkCode: number | undefined
|
||||
if (params.homeMobileNetworkCode) {
|
||||
const parsed = Number.parseInt(params.homeMobileNetworkCode, 10)
|
||||
homeMobileNetworkCode = Number.isNaN(parsed) ? undefined : parsed
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
address,
|
||||
@@ -567,10 +666,15 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
considerIp,
|
||||
days,
|
||||
plantsDescription,
|
||||
maxResultCount,
|
||||
homeMobileCountryCode,
|
||||
homeMobileNetworkCode,
|
||||
requiredQuality: params.requiredQuality || undefined,
|
||||
type: params.placeType || undefined,
|
||||
includedTypes: params.placeType ? [params.placeType] : undefined,
|
||||
avoid: params.avoid || undefined,
|
||||
radioType: params.radioType || undefined,
|
||||
units: operation === 'speed_limits' ? params.speedUnits || undefined : params.units,
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -593,6 +697,12 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
locationBias: { type: 'string', description: 'Location bias for search' },
|
||||
radius: { type: 'string', description: 'Search radius in meters' },
|
||||
placeType: { type: 'string', description: 'Place type filter' },
|
||||
pageToken: { type: 'string', description: 'Token to fetch the next page of place results' },
|
||||
maxResultCount: { type: 'string', description: 'Maximum number of nearby results to return' },
|
||||
rankPreference: {
|
||||
type: 'string',
|
||||
description: 'Nearby results ranking (POPULARITY, DISTANCE)',
|
||||
},
|
||||
placeId: { type: 'string', description: 'Google Place ID' },
|
||||
fields: { type: 'string', description: 'Fields to retrieve' },
|
||||
units: { type: 'string', description: 'Unit system' },
|
||||
@@ -601,18 +711,30 @@ export const GoogleMapsBlock: BlockConfig = {
|
||||
path: { type: 'string', description: 'Pipe-separated lat,lng coordinates' },
|
||||
interpolate: { type: 'boolean', description: 'Interpolate points along road' },
|
||||
placeIds: { type: 'string', description: 'Pipe-separated Place IDs for speed limits' },
|
||||
speedUnits: { type: 'string', description: 'Units for speed limit results (KPH, MPH)' },
|
||||
addressToValidate: { type: 'string', description: 'Address to validate' },
|
||||
regionCode: { type: 'string', description: 'ISO country code for address' },
|
||||
regionCode: { type: 'string', description: 'ISO country code for address or nearby search' },
|
||||
locality: { type: 'string', description: 'City name hint' },
|
||||
enableUspsCass: { type: 'boolean', description: 'Enable USPS CASS validation' },
|
||||
considerIp: { type: 'boolean', description: 'Use IP for geolocation' },
|
||||
homeMobileCountryCode: { type: 'string', description: 'Home network mobile country code' },
|
||||
homeMobileNetworkCode: { type: 'string', description: 'Home network mobile network code' },
|
||||
radioType: { type: 'string', description: 'Radio type (lte, gsm, etc.)' },
|
||||
carrier: { type: 'string', description: 'Carrier name' },
|
||||
wifiAccessPoints: { type: 'string', description: 'WiFi access points JSON' },
|
||||
cellTowers: { type: 'string', description: 'Cell towers JSON' },
|
||||
aqLatitude: { type: 'string', description: 'Latitude for air quality, pollen, or solar' },
|
||||
aqLongitude: { type: 'string', description: 'Longitude for air quality, pollen, or solar' },
|
||||
languageCode: { type: 'string', description: 'Language code for air quality or pollen' },
|
||||
aqLatitude: {
|
||||
type: 'string',
|
||||
description: 'Latitude for air quality, pollen, solar, or nearby search',
|
||||
},
|
||||
aqLongitude: {
|
||||
type: 'string',
|
||||
description: 'Longitude for air quality, pollen, solar, or nearby search',
|
||||
},
|
||||
languageCode: {
|
||||
type: 'string',
|
||||
description: 'Language code for air quality, pollen, or nearby search',
|
||||
},
|
||||
days: { type: 'string', description: 'Number of pollen forecast days (1-5)' },
|
||||
plantsDescription: { type: 'boolean', description: 'Include detailed plant descriptions' },
|
||||
requiredQuality: { type: 'string', description: 'Minimum solar imagery quality' },
|
||||
@@ -807,5 +929,12 @@ export const GoogleMapsBlockMeta = {
|
||||
content:
|
||||
'# Calculate Travel Distances\n\nGet a distance matrix from an origin to multiple destinations.\n\n## Steps\n1. Set the Origin and provide Destinations as a pipe-separated list (e.g., "New York, NY|Boston, MA").\n2. Choose Travel Mode and Units; optionally set features to Avoid.\n3. Run the Distance Matrix operation.\n4. Read each row for distance and duration to each destination.\n\n## Output\nA table of destinations sorted by travel time or distance, each with distance text and duration text. Useful for picking the nearest option or planning routes.',
|
||||
},
|
||||
{
|
||||
name: 'find-places-nearby',
|
||||
description:
|
||||
'Find places of a given type within a radius of a coordinate, ranked by popularity or distance, without needing a text query.',
|
||||
content:
|
||||
"# Find Places Nearby\n\nDiscover places around a fixed point (e.g., a store, delivery stop, or event venue) by type and radius, without a free-text search.\n\n## Steps\n1. Set the Latitude/Longitude of the center point and a Radius (meters, up to 50000).\n2. Optionally constrain by Place Type (restaurant, hotel, pharmacy, etc.) and choose Rank By (Popularity or Distance).\n3. Set Max Results if you only need the top few.\n4. Run the Nearby Places operation.\n\n## Output\nA list of nearby places with name, address, coordinates, rating, number of ratings, open-now status, and business status, ordered per the chosen ranking. Use Place Details with a result's place ID for phone/website/hours.",
|
||||
},
|
||||
],
|
||||
} as const satisfies BlockMeta
|
||||
|
||||
@@ -78,7 +78,7 @@ export const googleMapsElevationTool: ToolConfig<
|
||||
elevation: result.elevation,
|
||||
lat: result.location.lat,
|
||||
lng: result.location.lng,
|
||||
resolution: result.resolution,
|
||||
resolution: result.resolution ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -100,6 +100,7 @@ export const googleMapsElevationTool: ToolConfig<
|
||||
type: 'number',
|
||||
description:
|
||||
'Maximum distance between data points (meters) from which elevation was interpolated',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { googleMapsElevationTool } from '@/tools/google_maps/elevation'
|
||||
import { googleMapsGeocodeTool } from '@/tools/google_maps/geocode'
|
||||
import { googleMapsGeolocateTool } from '@/tools/google_maps/geolocate'
|
||||
import { googleMapsPlaceDetailsTool } from '@/tools/google_maps/place_details'
|
||||
import { googleMapsPlacesNearbyTool } from '@/tools/google_maps/places_nearby'
|
||||
import { googleMapsPlacesSearchTool } from '@/tools/google_maps/places_search'
|
||||
import { googleMapsPollenTool } from '@/tools/google_maps/pollen'
|
||||
import { googleMapsReverseGeocodeTool } from '@/tools/google_maps/reverse_geocode'
|
||||
@@ -22,6 +23,7 @@ export {
|
||||
googleMapsGeocodeTool,
|
||||
googleMapsGeolocateTool,
|
||||
googleMapsPlaceDetailsTool,
|
||||
googleMapsPlacesNearbyTool,
|
||||
googleMapsPlacesSearchTool,
|
||||
googleMapsPollenTool,
|
||||
googleMapsReverseGeocodeTool,
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import type {
|
||||
GoogleMapsPlacesNearbyParams,
|
||||
GoogleMapsPlacesNearbyResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsPlacesNearbyTool: ToolConfig<
|
||||
GoogleMapsPlacesNearbyParams,
|
||||
GoogleMapsPlacesNearbyResponse
|
||||
> = {
|
||||
id: 'google_maps_places_nearby',
|
||||
name: 'Google Maps Places Nearby Search',
|
||||
description: 'Search for places of a given type within a radius of a location',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Latitude of the center point to search around',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longitude of the center point to search around',
|
||||
},
|
||||
radius: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search radius in meters (up to 50000)',
|
||||
},
|
||||
includedTypes: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Place types to include in the results (e.g., restaurant, cafe)',
|
||||
},
|
||||
maxResultCount: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return (1-20, defaults to 20)',
|
||||
},
|
||||
rankPreference: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'How to rank results: POPULARITY (default) or DISTANCE',
|
||||
},
|
||||
languageCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for the response (e.g., en, es)',
|
||||
},
|
||||
regionCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Region bias as a ccTLD code (e.g., us, uk)',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.032,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://places.googleapis.com/v1/places:searchNearby',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-Api-Key': params.apiKey.trim(),
|
||||
'X-Goog-FieldMask':
|
||||
'places.id,places.displayName,places.formattedAddress,places.location,places.types,places.rating,places.userRatingCount,places.priceLevel,places.currentOpeningHours.openNow,places.businessStatus',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: {
|
||||
locationRestriction: {
|
||||
circle: { center: { latitude: number; longitude: number }; radius: number }
|
||||
}
|
||||
includedTypes?: string[]
|
||||
maxResultCount?: number
|
||||
rankPreference?: string
|
||||
languageCode?: string
|
||||
regionCode?: string
|
||||
} = {
|
||||
locationRestriction: {
|
||||
circle: {
|
||||
center: { latitude: params.lat, longitude: params.lng },
|
||||
radius: params.radius,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if (params.includedTypes && params.includedTypes.length > 0) {
|
||||
body.includedTypes = params.includedTypes
|
||||
}
|
||||
if (params.maxResultCount) {
|
||||
body.maxResultCount = params.maxResultCount
|
||||
}
|
||||
if (params.rankPreference) {
|
||||
body.rankPreference = params.rankPreference
|
||||
}
|
||||
if (params.languageCode) {
|
||||
body.languageCode = params.languageCode.trim()
|
||||
}
|
||||
if (params.regionCode) {
|
||||
body.regionCode = params.regionCode.trim()
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok || data.error) {
|
||||
throw new Error(`Places Nearby Search failed: ${data.error?.message || response.statusText}`)
|
||||
}
|
||||
|
||||
const places = (data.places || []).map(
|
||||
(place: {
|
||||
id: string
|
||||
displayName?: { text?: string }
|
||||
formattedAddress?: string
|
||||
location?: { latitude: number; longitude: number }
|
||||
types?: string[]
|
||||
rating?: number
|
||||
userRatingCount?: number
|
||||
priceLevel?: string
|
||||
currentOpeningHours?: { openNow?: boolean }
|
||||
businessStatus?: string
|
||||
}) => ({
|
||||
placeId: place.id,
|
||||
name: place.displayName?.text || '',
|
||||
formattedAddress: place.formattedAddress ?? null,
|
||||
lat: place.location?.latitude ?? null,
|
||||
lng: place.location?.longitude ?? null,
|
||||
types: place.types ?? [],
|
||||
rating: place.rating ?? null,
|
||||
userRatingsTotal: place.userRatingCount ?? null,
|
||||
priceLevel: place.priceLevel ?? null,
|
||||
openNow: place.currentOpeningHours?.openNow ?? null,
|
||||
businessStatus: place.businessStatus ?? null,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
places,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
places: {
|
||||
type: 'array',
|
||||
description: 'List of places found near the given location',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
placeId: { type: 'string', description: 'Google Place resource ID' },
|
||||
name: { type: 'string', description: 'Place name' },
|
||||
formattedAddress: { type: 'string', description: 'Formatted address', optional: true },
|
||||
lat: { type: 'number', description: 'Latitude', optional: true },
|
||||
lng: { type: 'number', description: 'Longitude', optional: true },
|
||||
types: { type: 'array', description: 'Place types' },
|
||||
rating: { type: 'number', description: 'Average rating (1-5)', optional: true },
|
||||
userRatingsTotal: { type: 'number', description: 'Number of ratings', optional: true },
|
||||
priceLevel: {
|
||||
type: 'string',
|
||||
description: 'Price level (e.g., PRICE_LEVEL_MODERATE)',
|
||||
optional: true,
|
||||
},
|
||||
openNow: { type: 'boolean', description: 'Whether currently open', optional: true },
|
||||
businessStatus: { type: 'string', description: 'Business status', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -56,6 +56,13 @@ export const googleMapsPlacesSearchTool: ToolConfig<
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Region bias as a ccTLD code (e.g., us, uk)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Token from a previous search response to fetch the next page of results. Wait a couple seconds after receiving the token before using it, or the API returns INVALID_REQUEST',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
@@ -93,6 +100,9 @@ export const googleMapsPlacesSearchTool: ToolConfig<
|
||||
if (params.region) {
|
||||
url.searchParams.set('region', params.region.trim())
|
||||
}
|
||||
if (params.pageToken) {
|
||||
url.searchParams.set('pagetoken', params.pageToken.trim())
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
|
||||
@@ -32,6 +32,12 @@ export const googleMapsSpeedLimitsTool: ToolConfig<
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of Place IDs for road segments (required if path not provided)',
|
||||
},
|
||||
units: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Units for the returned speed limits: KPH (default) or MPH',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
@@ -58,6 +64,10 @@ export const googleMapsSpeedLimitsTool: ToolConfig<
|
||||
}
|
||||
}
|
||||
|
||||
if (params.units) {
|
||||
url.searchParams.set('units', params.units)
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
|
||||
@@ -259,6 +259,7 @@ export interface GoogleMapsPlacesSearchParams {
|
||||
type?: string
|
||||
language?: string
|
||||
region?: string
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsPlacesSearchResponse extends ToolResponse {
|
||||
@@ -268,6 +269,42 @@ export interface GoogleMapsPlacesSearchResponse extends ToolResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Places Nearby Search
|
||||
// ============================================================================
|
||||
|
||||
interface NearbyPlaceResult {
|
||||
placeId: string
|
||||
name: string
|
||||
formattedAddress: string | null
|
||||
lat: number | null
|
||||
lng: number | null
|
||||
types: string[]
|
||||
rating: number | null
|
||||
userRatingsTotal: number | null
|
||||
priceLevel: string | null
|
||||
openNow: boolean | null
|
||||
businessStatus: string | null
|
||||
}
|
||||
|
||||
export interface GoogleMapsPlacesNearbyParams {
|
||||
apiKey: string
|
||||
lat: number
|
||||
lng: number
|
||||
radius: number
|
||||
includedTypes?: string[]
|
||||
maxResultCount?: number
|
||||
rankPreference?: 'POPULARITY' | 'DISTANCE'
|
||||
languageCode?: string
|
||||
regionCode?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsPlacesNearbyResponse extends ToolResponse {
|
||||
output: {
|
||||
places: NearbyPlaceResult[]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Place Details
|
||||
// ============================================================================
|
||||
@@ -336,7 +373,7 @@ export interface GoogleMapsElevationResponse extends ToolResponse {
|
||||
elevation: number
|
||||
lat: number
|
||||
lng: number
|
||||
resolution: number
|
||||
resolution: number | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,6 +425,7 @@ export interface GoogleMapsSpeedLimitsParams {
|
||||
apiKey: string
|
||||
path?: string
|
||||
placeIds?: string[]
|
||||
units?: 'KPH' | 'MPH'
|
||||
}
|
||||
|
||||
export interface GoogleMapsSpeedLimitsResponse extends ToolResponse {
|
||||
|
||||
@@ -1442,6 +1442,7 @@ import {
|
||||
googleMapsGeocodeTool,
|
||||
googleMapsGeolocateTool,
|
||||
googleMapsPlaceDetailsTool,
|
||||
googleMapsPlacesNearbyTool,
|
||||
googleMapsPlacesSearchTool,
|
||||
googleMapsPollenTool,
|
||||
googleMapsReverseGeocodeTool,
|
||||
@@ -6393,6 +6394,7 @@ export const tools: Record<string, ToolConfig> = {
|
||||
google_maps_geocode: googleMapsGeocodeTool,
|
||||
google_maps_geolocate: googleMapsGeolocateTool,
|
||||
google_maps_place_details: googleMapsPlaceDetailsTool,
|
||||
google_maps_places_nearby: googleMapsPlacesNearbyTool,
|
||||
google_maps_places_search: googleMapsPlacesSearchTool,
|
||||
google_maps_pollen: googleMapsPollenTool,
|
||||
google_maps_reverse_geocode: googleMapsReverseGeocodeTool,
|
||||
|
||||
Reference in New Issue
Block a user