chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
import type {
|
||||
GoogleMapsAirQualityParams,
|
||||
GoogleMapsAirQualityResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsAirQualityTool: ToolConfig<
|
||||
GoogleMapsAirQualityParams,
|
||||
GoogleMapsAirQualityResponse
|
||||
> = {
|
||||
id: 'google_maps_air_quality',
|
||||
name: 'Google Maps Air Quality',
|
||||
description: 'Get current air quality data for a location',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Air Quality API enabled',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
languageCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for the response (e.g., "en", "es")',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.005,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
return `https://airquality.googleapis.com/v1/currentConditions:lookup?key=${params.apiKey.trim()}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: {
|
||||
location: { latitude: number; longitude: number }
|
||||
extraComputations: string[]
|
||||
languageCode?: string
|
||||
} = {
|
||||
location: {
|
||||
latitude: params.lat,
|
||||
longitude: params.lng,
|
||||
},
|
||||
extraComputations: [
|
||||
'HEALTH_RECOMMENDATIONS',
|
||||
'DOMINANT_POLLUTANT_CONCENTRATION',
|
||||
'POLLUTANT_CONCENTRATION',
|
||||
'LOCAL_AQI',
|
||||
'POLLUTANT_ADDITIONAL_INFO',
|
||||
],
|
||||
}
|
||||
|
||||
if (params.languageCode) {
|
||||
body.languageCode = params.languageCode.trim()
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`Air Quality failed: ${data.error.message || 'Unknown error'}`)
|
||||
}
|
||||
|
||||
const indexes = (data.indexes || []).map(
|
||||
(index: {
|
||||
code: string
|
||||
displayName: string
|
||||
aqi: number
|
||||
aqiDisplay: string
|
||||
color: { red?: number; green?: number; blue?: number }
|
||||
category: string
|
||||
dominantPollutant: string
|
||||
}) => ({
|
||||
code: index.code,
|
||||
displayName: index.displayName,
|
||||
aqi: index.aqi,
|
||||
aqiDisplay: index.aqiDisplay,
|
||||
color: {
|
||||
red: index.color?.red || 0,
|
||||
green: index.color?.green || 0,
|
||||
blue: index.color?.blue || 0,
|
||||
},
|
||||
category: index.category,
|
||||
dominantPollutant: index.dominantPollutant,
|
||||
})
|
||||
)
|
||||
|
||||
const pollutants = (data.pollutants || []).map(
|
||||
(pollutant: {
|
||||
code: string
|
||||
displayName: string
|
||||
fullName: string
|
||||
concentration: { value: number; units: string }
|
||||
additionalInfo?: { sources: string; effects: string }
|
||||
}) => ({
|
||||
code: pollutant.code,
|
||||
displayName: pollutant.displayName,
|
||||
fullName: pollutant.fullName,
|
||||
concentration: {
|
||||
value: pollutant.concentration?.value || 0,
|
||||
units: pollutant.concentration?.units || '',
|
||||
},
|
||||
additionalInfo: pollutant.additionalInfo
|
||||
? {
|
||||
sources: pollutant.additionalInfo.sources,
|
||||
effects: pollutant.additionalInfo.effects,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
)
|
||||
|
||||
const healthRecs = data.healthRecommendations
|
||||
const healthRecommendations = healthRecs
|
||||
? {
|
||||
generalPopulation: healthRecs.generalPopulation || '',
|
||||
elderly: healthRecs.elderly || '',
|
||||
lungDiseasePopulation: healthRecs.lungDiseasePopulation || '',
|
||||
heartDiseasePopulation: healthRecs.heartDiseasePopulation || '',
|
||||
athletes: healthRecs.athletes || '',
|
||||
pregnantWomen: healthRecs.pregnantWomen || '',
|
||||
children: healthRecs.children || '',
|
||||
}
|
||||
: null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
dateTime: data.dateTime || '',
|
||||
regionCode: data.regionCode || '',
|
||||
indexes,
|
||||
pollutants,
|
||||
healthRecommendations,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
dateTime: {
|
||||
type: 'string',
|
||||
description: 'Timestamp of the air quality data',
|
||||
},
|
||||
regionCode: {
|
||||
type: 'string',
|
||||
description: 'Region code for the location',
|
||||
},
|
||||
indexes: {
|
||||
type: 'array',
|
||||
description: 'Array of air quality indexes',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string', description: 'Index code (e.g., "uaqi", "usa_epa")' },
|
||||
displayName: { type: 'string', description: 'Display name of the index' },
|
||||
aqi: { type: 'number', description: 'Air quality index value' },
|
||||
aqiDisplay: { type: 'string', description: 'Formatted AQI display string' },
|
||||
color: {
|
||||
type: 'object',
|
||||
description: 'RGB color for the AQI level',
|
||||
properties: {
|
||||
red: { type: 'number' },
|
||||
green: { type: 'number' },
|
||||
blue: { type: 'number' },
|
||||
},
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
description: 'Category description (e.g., "Good", "Moderate")',
|
||||
},
|
||||
dominantPollutant: { type: 'string', description: 'The dominant pollutant' },
|
||||
},
|
||||
},
|
||||
},
|
||||
pollutants: {
|
||||
type: 'array',
|
||||
description: 'Array of pollutant concentrations',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string', description: 'Pollutant code (e.g., "pm25", "o3")' },
|
||||
displayName: { type: 'string', description: 'Display name' },
|
||||
fullName: { type: 'string', description: 'Full pollutant name' },
|
||||
concentration: {
|
||||
type: 'object',
|
||||
description: 'Concentration info',
|
||||
properties: {
|
||||
value: { type: 'number', description: 'Concentration value' },
|
||||
units: { type: 'string', description: 'Units (e.g., "PARTS_PER_BILLION")' },
|
||||
},
|
||||
},
|
||||
additionalInfo: {
|
||||
type: 'object',
|
||||
description: 'Additional info about sources and effects',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
healthRecommendations: {
|
||||
type: 'object',
|
||||
description: 'Health recommendations for different populations',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import type {
|
||||
GoogleMapsDirectionsParams,
|
||||
GoogleMapsDirectionsResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsDirectionsTool: ToolConfig<
|
||||
GoogleMapsDirectionsParams,
|
||||
GoogleMapsDirectionsResponse
|
||||
> = {
|
||||
id: 'google_maps_directions',
|
||||
name: 'Google Maps Directions',
|
||||
description: 'Get directions and route information between two locations',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
origin: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Starting location (address or lat,lng)',
|
||||
},
|
||||
destination: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Destination location (address or lat,lng)',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Travel mode: driving, walking, bicycling, or transit',
|
||||
},
|
||||
avoid: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Features to avoid: tolls, highways, or ferries',
|
||||
},
|
||||
waypoints: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of intermediate waypoints',
|
||||
},
|
||||
units: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Unit system: metric or imperial',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.005,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/directions/json')
|
||||
url.searchParams.set('origin', params.origin.trim())
|
||||
url.searchParams.set('destination', params.destination.trim())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
|
||||
if (params.mode) {
|
||||
url.searchParams.set('mode', params.mode)
|
||||
}
|
||||
if (params.avoid) {
|
||||
url.searchParams.set('avoid', params.avoid)
|
||||
}
|
||||
if (params.waypoints && params.waypoints.length > 0) {
|
||||
url.searchParams.set('waypoints', params.waypoints.join('|'))
|
||||
}
|
||||
if (params.units) {
|
||||
url.searchParams.set('units', params.units)
|
||||
}
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Directions request failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const routes = data.routes.map(
|
||||
(route: {
|
||||
summary: string
|
||||
legs: Array<{
|
||||
start_address: string
|
||||
end_address: string
|
||||
start_location: { lat: number; lng: number }
|
||||
end_location: { lat: number; lng: number }
|
||||
distance: { text: string; value: number }
|
||||
duration: { text: string; value: number }
|
||||
steps: Array<{
|
||||
html_instructions: string
|
||||
distance: { text: string; value: number }
|
||||
duration: { text: string; value: number }
|
||||
start_location: { lat: number; lng: number }
|
||||
end_location: { lat: number; lng: number }
|
||||
travel_mode: string
|
||||
maneuver?: string
|
||||
}>
|
||||
}>
|
||||
overview_polyline: { points: string }
|
||||
warnings: string[]
|
||||
waypoint_order: number[]
|
||||
}) => ({
|
||||
summary: route.summary,
|
||||
legs: route.legs.map((leg) => ({
|
||||
startAddress: leg.start_address,
|
||||
endAddress: leg.end_address,
|
||||
startLocation: {
|
||||
lat: leg.start_location.lat,
|
||||
lng: leg.start_location.lng,
|
||||
},
|
||||
endLocation: {
|
||||
lat: leg.end_location.lat,
|
||||
lng: leg.end_location.lng,
|
||||
},
|
||||
distanceText: leg.distance.text,
|
||||
distanceMeters: leg.distance.value,
|
||||
durationText: leg.duration.text,
|
||||
durationSeconds: leg.duration.value,
|
||||
steps: leg.steps.map((step) => ({
|
||||
instruction: step.html_instructions.replace(/<[^>]*>/g, ''),
|
||||
distanceText: step.distance.text,
|
||||
distanceMeters: step.distance.value,
|
||||
durationText: step.duration.text,
|
||||
durationSeconds: step.duration.value,
|
||||
startLocation: {
|
||||
lat: step.start_location.lat,
|
||||
lng: step.start_location.lng,
|
||||
},
|
||||
endLocation: {
|
||||
lat: step.end_location.lat,
|
||||
lng: step.end_location.lng,
|
||||
},
|
||||
travelMode: step.travel_mode,
|
||||
maneuver: step.maneuver ?? null,
|
||||
})),
|
||||
})),
|
||||
overviewPolyline: route.overview_polyline.points,
|
||||
warnings: route.warnings ?? [],
|
||||
waypointOrder: route.waypoint_order ?? [],
|
||||
})
|
||||
)
|
||||
|
||||
const primaryRoute = routes[0]
|
||||
const primaryLeg = primaryRoute?.legs[0]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
routes,
|
||||
distanceText: primaryLeg?.distanceText ?? '',
|
||||
distanceMeters: primaryLeg?.distanceMeters ?? 0,
|
||||
durationText: primaryLeg?.durationText ?? '',
|
||||
durationSeconds: primaryLeg?.durationSeconds ?? 0,
|
||||
startAddress: primaryLeg?.startAddress ?? '',
|
||||
endAddress: primaryLeg?.endAddress ?? '',
|
||||
steps: primaryLeg?.steps ?? [],
|
||||
polyline: primaryRoute?.overviewPolyline ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
routes: {
|
||||
type: 'array',
|
||||
description: 'All available routes',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
summary: { type: 'string', description: 'Route summary (main road names)' },
|
||||
legs: { type: 'array', description: 'Route legs (segments between waypoints)' },
|
||||
overviewPolyline: {
|
||||
type: 'string',
|
||||
description: 'Encoded polyline for the entire route',
|
||||
},
|
||||
warnings: { type: 'array', description: 'Route warnings' },
|
||||
waypointOrder: { type: 'array', description: 'Optimized waypoint order (if requested)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
distanceText: {
|
||||
type: 'string',
|
||||
description: 'Total distance as human-readable text (e.g., "5.2 km")',
|
||||
},
|
||||
distanceMeters: {
|
||||
type: 'number',
|
||||
description: 'Total distance in meters',
|
||||
},
|
||||
durationText: {
|
||||
type: 'string',
|
||||
description: 'Total duration as human-readable text (e.g., "15 mins")',
|
||||
},
|
||||
durationSeconds: {
|
||||
type: 'number',
|
||||
description: 'Total duration in seconds',
|
||||
},
|
||||
startAddress: {
|
||||
type: 'string',
|
||||
description: 'Resolved starting address',
|
||||
},
|
||||
endAddress: {
|
||||
type: 'string',
|
||||
description: 'Resolved ending address',
|
||||
},
|
||||
steps: {
|
||||
type: 'array',
|
||||
description: 'Turn-by-turn navigation instructions',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
instruction: { type: 'string', description: 'Navigation instruction (HTML stripped)' },
|
||||
distanceText: { type: 'string', description: 'Step distance as text' },
|
||||
distanceMeters: { type: 'number', description: 'Step distance in meters' },
|
||||
durationText: { type: 'string', description: 'Step duration as text' },
|
||||
durationSeconds: { type: 'number', description: 'Step duration in seconds' },
|
||||
startLocation: { type: 'object', description: 'Step start coordinates' },
|
||||
endLocation: { type: 'object', description: 'Step end coordinates' },
|
||||
travelMode: { type: 'string', description: 'Travel mode for this step' },
|
||||
maneuver: {
|
||||
type: 'string',
|
||||
description: 'Maneuver type (turn-left, etc.)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
polyline: {
|
||||
type: 'string',
|
||||
description: 'Encoded polyline for the primary route',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import type {
|
||||
GoogleMapsDistanceMatrixParams,
|
||||
GoogleMapsDistanceMatrixResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsDistanceMatrixTool: ToolConfig<
|
||||
GoogleMapsDistanceMatrixParams,
|
||||
GoogleMapsDistanceMatrixResponse
|
||||
> = {
|
||||
id: 'google_maps_distance_matrix',
|
||||
name: 'Google Maps Distance Matrix',
|
||||
description: 'Calculate travel distance and time between multiple origins and destinations',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
origin: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Origin location (address or lat,lng)',
|
||||
},
|
||||
destinations: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of destination locations',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Travel mode: driving, walking, bicycling, or transit',
|
||||
},
|
||||
avoid: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Features to avoid: tolls, highways, or ferries',
|
||||
},
|
||||
units: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Unit system: metric or imperial',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.005,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/distancematrix/json')
|
||||
url.searchParams.set('origins', params.origin.trim())
|
||||
url.searchParams.set('destinations', params.destinations.join('|'))
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
|
||||
if (params.mode) {
|
||||
url.searchParams.set('mode', params.mode)
|
||||
}
|
||||
if (params.avoid) {
|
||||
url.searchParams.set('avoid', params.avoid)
|
||||
}
|
||||
if (params.units) {
|
||||
url.searchParams.set('units', params.units)
|
||||
}
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Distance matrix request failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const rows = data.rows.map(
|
||||
(row: {
|
||||
elements: Array<{
|
||||
distance?: { text: string; value: number }
|
||||
duration?: { text: string; value: number }
|
||||
duration_in_traffic?: { text: string; value: number }
|
||||
status: string
|
||||
}>
|
||||
}) => ({
|
||||
elements: row.elements.map((element) => ({
|
||||
distanceText: element.distance?.text ?? 'N/A',
|
||||
distanceMeters: element.distance?.value ?? 0,
|
||||
durationText: element.duration?.text ?? 'N/A',
|
||||
durationSeconds: element.duration?.value ?? 0,
|
||||
durationInTrafficText: element.duration_in_traffic?.text ?? null,
|
||||
durationInTrafficSeconds: element.duration_in_traffic?.value ?? null,
|
||||
status: element.status,
|
||||
})),
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
originAddresses: data.origin_addresses ?? [],
|
||||
destinationAddresses: data.destination_addresses ?? [],
|
||||
rows,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
originAddresses: {
|
||||
type: 'array',
|
||||
description: 'Resolved origin addresses',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
destinationAddresses: {
|
||||
type: 'array',
|
||||
description: 'Resolved destination addresses',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
rows: {
|
||||
type: 'array',
|
||||
description: 'Distance matrix rows (one per origin)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
elements: {
|
||||
type: 'array',
|
||||
description: 'Elements (one per destination)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
distanceText: { type: 'string', description: 'Distance as text (e.g., "5.2 km")' },
|
||||
distanceMeters: { type: 'number', description: 'Distance in meters' },
|
||||
durationText: { type: 'string', description: 'Duration as text (e.g., "15 mins")' },
|
||||
durationSeconds: { type: 'number', description: 'Duration in seconds' },
|
||||
durationInTrafficText: {
|
||||
type: 'string',
|
||||
description: 'Duration in traffic as text',
|
||||
optional: true,
|
||||
},
|
||||
durationInTrafficSeconds: {
|
||||
type: 'number',
|
||||
description: 'Duration in traffic in seconds',
|
||||
optional: true,
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Element status (OK, NOT_FOUND, ZERO_RESULTS)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
GoogleMapsElevationParams,
|
||||
GoogleMapsElevationResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsElevationTool: ToolConfig<
|
||||
GoogleMapsElevationParams,
|
||||
GoogleMapsElevationResponse
|
||||
> = {
|
||||
id: 'google_maps_elevation',
|
||||
name: 'Google Maps Elevation',
|
||||
description: 'Get elevation data for 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 coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.005,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/elevation/json')
|
||||
url.searchParams.set('locations', `${params.lat},${params.lng}`)
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Elevation request failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = data.results[0]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
elevation: result.elevation,
|
||||
lat: result.location.lat,
|
||||
lng: result.location.lng,
|
||||
resolution: result.resolution ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
elevation: {
|
||||
type: 'number',
|
||||
description: 'Elevation in meters above sea level (negative for below)',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
description: 'Latitude of the elevation sample',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
description: 'Longitude of the elevation sample',
|
||||
},
|
||||
resolution: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Maximum distance between data points (meters) from which elevation was interpolated',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { GoogleMapsGeocodeParams, GoogleMapsGeocodeResponse } from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsGeocodeTool: ToolConfig<GoogleMapsGeocodeParams, GoogleMapsGeocodeResponse> =
|
||||
{
|
||||
id: 'google_maps_geocode',
|
||||
name: 'Google Maps Geocode',
|
||||
description: 'Convert an address into geographic coordinates (latitude and longitude)',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
address: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The address to geocode',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
region: {
|
||||
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.005,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/geocode/json')
|
||||
url.searchParams.set('address', params.address.trim())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
if (params.region) {
|
||||
url.searchParams.set('region', params.region.trim())
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Geocoding failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = data.results[0]
|
||||
const location = result.geometry.location
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
formattedAddress: result.formatted_address,
|
||||
lat: location.lat,
|
||||
lng: location.lng,
|
||||
location: {
|
||||
lat: location.lat,
|
||||
lng: location.lng,
|
||||
},
|
||||
placeId: result.place_id,
|
||||
addressComponents: (result.address_components || []).map(
|
||||
(comp: { long_name: string; short_name: string; types: string[] }) => ({
|
||||
longName: comp.long_name,
|
||||
shortName: comp.short_name,
|
||||
types: comp.types,
|
||||
})
|
||||
),
|
||||
locationType: result.geometry.location_type,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
formattedAddress: {
|
||||
type: 'string',
|
||||
description: 'The formatted address string',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
location: {
|
||||
type: 'json',
|
||||
description: 'Location object with lat and lng',
|
||||
},
|
||||
placeId: {
|
||||
type: 'string',
|
||||
description: 'Google Place ID for this location',
|
||||
},
|
||||
addressComponents: {
|
||||
type: 'array',
|
||||
description: 'Detailed address components',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
longName: { type: 'string', description: 'Full name of the component' },
|
||||
shortName: { type: 'string', description: 'Abbreviated name' },
|
||||
types: { type: 'array', description: 'Component types' },
|
||||
},
|
||||
},
|
||||
},
|
||||
locationType: {
|
||||
type: 'string',
|
||||
description: 'Location accuracy type (ROOFTOP, RANGE_INTERPOLATED, etc.)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import type {
|
||||
GoogleMapsGeolocateParams,
|
||||
GoogleMapsGeolocateResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsGeolocateTool: ToolConfig<
|
||||
GoogleMapsGeolocateParams,
|
||||
GoogleMapsGeolocateResponse
|
||||
> = {
|
||||
id: 'google_maps_geolocate',
|
||||
name: 'Google Maps Geolocate',
|
||||
description: 'Geolocate a device using WiFi access points, cell towers, or IP address',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Geolocation API enabled',
|
||||
},
|
||||
homeMobileCountryCode: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Home mobile country code (MCC)',
|
||||
},
|
||||
homeMobileNetworkCode: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Home mobile network code (MNC)',
|
||||
},
|
||||
radioType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Radio type: lte, gsm, cdma, wcdma, or nr',
|
||||
},
|
||||
carrier: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Carrier name',
|
||||
},
|
||||
considerIp: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to use IP address for geolocation (default: true)',
|
||||
},
|
||||
cellTowers: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of cell tower objects with cellId, locationAreaCode, mobileCountryCode, mobileNetworkCode',
|
||||
},
|
||||
wifiAccessPoints: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of WiFi access point objects with macAddress (required), signalStrength, etc.',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.005,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
return `https://www.googleapis.com/geolocation/v1/geolocate?key=${params.apiKey.trim()}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: {
|
||||
homeMobileCountryCode?: number
|
||||
homeMobileNetworkCode?: number
|
||||
radioType?: string
|
||||
carrier?: string
|
||||
considerIp?: boolean
|
||||
cellTowers?: Array<{
|
||||
cellId: number
|
||||
locationAreaCode: number
|
||||
mobileCountryCode: number
|
||||
mobileNetworkCode: number
|
||||
age?: number
|
||||
signalStrength?: number
|
||||
timingAdvance?: number
|
||||
}>
|
||||
wifiAccessPoints?: Array<{
|
||||
macAddress: string
|
||||
signalStrength?: number
|
||||
age?: number
|
||||
channel?: number
|
||||
signalToNoiseRatio?: number
|
||||
}>
|
||||
} = {}
|
||||
|
||||
if (params.homeMobileCountryCode !== undefined) {
|
||||
body.homeMobileCountryCode = params.homeMobileCountryCode
|
||||
}
|
||||
|
||||
if (params.homeMobileNetworkCode !== undefined) {
|
||||
body.homeMobileNetworkCode = params.homeMobileNetworkCode
|
||||
}
|
||||
|
||||
if (params.radioType) {
|
||||
body.radioType = params.radioType
|
||||
}
|
||||
|
||||
if (params.carrier) {
|
||||
body.carrier = params.carrier
|
||||
}
|
||||
|
||||
if (params.considerIp !== undefined) {
|
||||
body.considerIp = params.considerIp
|
||||
}
|
||||
|
||||
if (params.cellTowers && params.cellTowers.length > 0) {
|
||||
body.cellTowers = params.cellTowers
|
||||
}
|
||||
|
||||
if (params.wifiAccessPoints && params.wifiAccessPoints.length > 0) {
|
||||
body.wifiAccessPoints = params.wifiAccessPoints
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`Geolocation failed: ${data.error.message || 'Unknown error'}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
lat: data.location?.lat || 0,
|
||||
lng: data.location?.lng || 0,
|
||||
accuracy: data.accuracy || 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
lat: {
|
||||
type: 'number',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
accuracy: {
|
||||
type: 'number',
|
||||
description: 'Accuracy radius in meters',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { googleMapsAirQualityTool } from '@/tools/google_maps/air_quality'
|
||||
import { googleMapsDirectionsTool } from '@/tools/google_maps/directions'
|
||||
import { googleMapsDistanceMatrixTool } from '@/tools/google_maps/distance_matrix'
|
||||
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'
|
||||
import { googleMapsSnapToRoadsTool } from '@/tools/google_maps/snap_to_roads'
|
||||
import { googleMapsSolarTool } from '@/tools/google_maps/solar'
|
||||
import { googleMapsSpeedLimitsTool } from '@/tools/google_maps/speed_limits'
|
||||
import { googleMapsTimezoneTool } from '@/tools/google_maps/timezone'
|
||||
import { googleMapsValidateAddressTool } from '@/tools/google_maps/validate_address'
|
||||
|
||||
export {
|
||||
googleMapsAirQualityTool,
|
||||
googleMapsDirectionsTool,
|
||||
googleMapsDistanceMatrixTool,
|
||||
googleMapsElevationTool,
|
||||
googleMapsGeocodeTool,
|
||||
googleMapsGeolocateTool,
|
||||
googleMapsPlaceDetailsTool,
|
||||
googleMapsPlacesNearbyTool,
|
||||
googleMapsPlacesSearchTool,
|
||||
googleMapsPollenTool,
|
||||
googleMapsReverseGeocodeTool,
|
||||
googleMapsSnapToRoadsTool,
|
||||
googleMapsSolarTool,
|
||||
googleMapsSpeedLimitsTool,
|
||||
googleMapsTimezoneTool,
|
||||
googleMapsValidateAddressTool,
|
||||
}
|
||||
|
||||
// Export types
|
||||
export * from './types'
|
||||
@@ -0,0 +1,288 @@
|
||||
import type {
|
||||
GoogleMapsPlaceDetailsParams,
|
||||
GoogleMapsPlaceDetailsResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsPlaceDetailsTool: ToolConfig<
|
||||
GoogleMapsPlaceDetailsParams,
|
||||
GoogleMapsPlaceDetailsResponse
|
||||
> = {
|
||||
id: 'google_maps_place_details',
|
||||
name: 'Google Maps Place Details',
|
||||
description: 'Get detailed information about a specific place',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
placeId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Place ID',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of fields to return',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.017,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/place/details/json')
|
||||
url.searchParams.set('place_id', params.placeId.trim())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
|
||||
// Default fields if not specified - comprehensive list
|
||||
const fields =
|
||||
params.fields ||
|
||||
'place_id,name,formatted_address,geometry,types,rating,user_ratings_total,price_level,website,formatted_phone_number,international_phone_number,opening_hours,reviews,photos,url,utc_offset,vicinity,business_status'
|
||||
url.searchParams.set('fields', fields)
|
||||
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Place details request failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const place = data.result
|
||||
|
||||
const reviews = (place.reviews || []).map(
|
||||
(review: {
|
||||
author_name: string
|
||||
author_url?: string
|
||||
profile_photo_url?: string
|
||||
rating: number
|
||||
text: string
|
||||
time: number
|
||||
relative_time_description: string
|
||||
}) => ({
|
||||
authorName: review.author_name,
|
||||
authorUrl: review.author_url ?? null,
|
||||
profilePhotoUrl: review.profile_photo_url ?? null,
|
||||
rating: review.rating,
|
||||
text: review.text,
|
||||
time: review.time,
|
||||
relativeTimeDescription: review.relative_time_description,
|
||||
})
|
||||
)
|
||||
|
||||
const photos = (place.photos || []).map(
|
||||
(photo: {
|
||||
photo_reference: string
|
||||
height: number
|
||||
width: number
|
||||
html_attributions: string[]
|
||||
}) => ({
|
||||
photoReference: photo.photo_reference,
|
||||
height: photo.height,
|
||||
width: photo.width,
|
||||
htmlAttributions: photo.html_attributions ?? [],
|
||||
})
|
||||
)
|
||||
|
||||
// Destructure opening hours
|
||||
const openNow = place.opening_hours?.open_now ?? null
|
||||
const weekdayText = place.opening_hours?.weekday_text ?? []
|
||||
|
||||
// Extract location
|
||||
const lat = place.geometry?.location?.lat ?? null
|
||||
const lng = place.geometry?.location?.lng ?? null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
placeId: place.place_id,
|
||||
name: place.name ?? null,
|
||||
formattedAddress: place.formatted_address ?? null,
|
||||
lat,
|
||||
lng,
|
||||
types: place.types ?? [],
|
||||
rating: place.rating ?? null,
|
||||
userRatingsTotal: place.user_ratings_total ?? null,
|
||||
priceLevel: place.price_level ?? null,
|
||||
website: place.website ?? null,
|
||||
phoneNumber: place.formatted_phone_number ?? null,
|
||||
internationalPhoneNumber: place.international_phone_number ?? null,
|
||||
openNow,
|
||||
weekdayText,
|
||||
reviews,
|
||||
photos,
|
||||
url: place.url ?? null,
|
||||
utcOffset: place.utc_offset ?? null,
|
||||
vicinity: place.vicinity ?? null,
|
||||
businessStatus: place.business_status ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
placeId: {
|
||||
type: 'string',
|
||||
description: 'Google Place ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Place name',
|
||||
optional: true,
|
||||
},
|
||||
formattedAddress: {
|
||||
type: 'string',
|
||||
description: 'Formatted street address',
|
||||
optional: true,
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
description: 'Latitude coordinate',
|
||||
optional: true,
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
description: 'Longitude coordinate',
|
||||
optional: true,
|
||||
},
|
||||
types: {
|
||||
type: 'array',
|
||||
description: 'Place types (e.g., restaurant, cafe)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
rating: {
|
||||
type: 'number',
|
||||
description: 'Average rating (1.0 to 5.0)',
|
||||
optional: true,
|
||||
},
|
||||
userRatingsTotal: {
|
||||
type: 'number',
|
||||
description: 'Total number of user ratings',
|
||||
optional: true,
|
||||
},
|
||||
priceLevel: {
|
||||
type: 'number',
|
||||
description: 'Price level (0=Free, 1=Inexpensive, 2=Moderate, 3=Expensive, 4=Very Expensive)',
|
||||
optional: true,
|
||||
},
|
||||
website: {
|
||||
type: 'string',
|
||||
description: 'Place website URL',
|
||||
optional: true,
|
||||
},
|
||||
phoneNumber: {
|
||||
type: 'string',
|
||||
description: 'Local formatted phone number',
|
||||
optional: true,
|
||||
},
|
||||
internationalPhoneNumber: {
|
||||
type: 'string',
|
||||
description: 'International formatted phone number',
|
||||
optional: true,
|
||||
},
|
||||
openNow: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the place is currently open',
|
||||
optional: true,
|
||||
},
|
||||
weekdayText: {
|
||||
type: 'array',
|
||||
description: 'Opening hours formatted by day of week',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
reviews: {
|
||||
type: 'array',
|
||||
description: 'User reviews (up to 5 most relevant)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
authorName: { type: 'string', description: 'Reviewer name' },
|
||||
authorUrl: { type: 'string', description: 'Reviewer profile URL', optional: true },
|
||||
profilePhotoUrl: { type: 'string', description: 'Reviewer photo URL', optional: true },
|
||||
rating: { type: 'number', description: 'Rating given (1-5)' },
|
||||
text: { type: 'string', description: 'Review text' },
|
||||
time: { type: 'number', description: 'Review timestamp (Unix epoch)' },
|
||||
relativeTimeDescription: {
|
||||
type: 'string',
|
||||
description: 'Relative time (e.g., "a month ago")',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
photos: {
|
||||
type: 'array',
|
||||
description: 'Place photos',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
photoReference: { type: 'string', description: 'Photo reference for Place Photos API' },
|
||||
height: { type: 'number', description: 'Photo height in pixels' },
|
||||
width: { type: 'number', description: 'Photo width in pixels' },
|
||||
htmlAttributions: { type: 'array', description: 'Required attributions' },
|
||||
},
|
||||
},
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Google Maps URL for the place',
|
||||
optional: true,
|
||||
},
|
||||
utcOffset: {
|
||||
type: 'number',
|
||||
description: 'UTC offset in minutes',
|
||||
optional: true,
|
||||
},
|
||||
vicinity: {
|
||||
type: 'string',
|
||||
description: 'Simplified address (neighborhood/street)',
|
||||
optional: true,
|
||||
},
|
||||
businessStatus: {
|
||||
type: 'string',
|
||||
description: 'Business status (OPERATIONAL, CLOSED_TEMPORARILY, CLOSED_PERMANENTLY)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import type {
|
||||
GoogleMapsPlacesSearchParams,
|
||||
GoogleMapsPlacesSearchResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsPlacesSearchTool: ToolConfig<
|
||||
GoogleMapsPlacesSearchParams,
|
||||
GoogleMapsPlacesSearchResponse
|
||||
> = {
|
||||
id: 'google_maps_places_search',
|
||||
name: 'Google Maps Places Search',
|
||||
description: 'Search for places using a text query',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query (e.g., "restaurants in Times Square")',
|
||||
},
|
||||
location: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Location to bias results towards ({lat, lng})',
|
||||
},
|
||||
radius: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search radius in meters',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Place type filter (e.g., restaurant, cafe, hotel)',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
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: {
|
||||
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: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/place/textsearch/json')
|
||||
url.searchParams.set('query', params.query.trim())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
|
||||
if (params.location) {
|
||||
url.searchParams.set('location', `${params.location.lat},${params.location.lng}`)
|
||||
}
|
||||
if (params.radius) {
|
||||
url.searchParams.set('radius', params.radius.toString())
|
||||
}
|
||||
if (params.type) {
|
||||
url.searchParams.set('type', params.type)
|
||||
}
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
if (params.region) {
|
||||
url.searchParams.set('region', params.region.trim())
|
||||
}
|
||||
if (params.pageToken) {
|
||||
url.searchParams.set('pagetoken', params.pageToken.trim())
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK' && data.status !== 'ZERO_RESULTS') {
|
||||
throw new Error(
|
||||
`Places search failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const places = (data.results || []).map(
|
||||
(place: {
|
||||
place_id: string
|
||||
name: string
|
||||
formatted_address: string
|
||||
geometry: { location: { lat: number; lng: number } }
|
||||
types: string[]
|
||||
rating?: number
|
||||
user_ratings_total?: number
|
||||
price_level?: number
|
||||
opening_hours?: { open_now: boolean }
|
||||
photos?: Array<{ photo_reference: string; height: number; width: number }>
|
||||
business_status?: string
|
||||
}) => ({
|
||||
placeId: place.place_id,
|
||||
name: place.name,
|
||||
formattedAddress: place.formatted_address,
|
||||
lat: place.geometry.location.lat,
|
||||
lng: place.geometry.location.lng,
|
||||
types: place.types ?? [],
|
||||
rating: place.rating ?? null,
|
||||
userRatingsTotal: place.user_ratings_total ?? null,
|
||||
priceLevel: place.price_level ?? null,
|
||||
openNow: place.opening_hours?.open_now ?? null,
|
||||
photoReference: place.photos?.[0]?.photo_reference ?? null,
|
||||
businessStatus: place.business_status ?? null,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
places,
|
||||
nextPageToken: data.next_page_token ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
places: {
|
||||
type: 'array',
|
||||
description: 'List of places found',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
placeId: { type: 'string', description: 'Google Place ID' },
|
||||
name: { type: 'string', description: 'Place name' },
|
||||
formattedAddress: { type: 'string', description: 'Formatted address' },
|
||||
lat: { type: 'number', description: 'Latitude' },
|
||||
lng: { type: 'number', description: 'Longitude' },
|
||||
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: 'number', description: 'Price level (0-4)', optional: true },
|
||||
openNow: { type: 'boolean', description: 'Whether currently open', optional: true },
|
||||
photoReference: {
|
||||
type: 'string',
|
||||
description: 'Photo reference for Photos API',
|
||||
optional: true,
|
||||
},
|
||||
businessStatus: { type: 'string', description: 'Business status', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import type { GoogleMapsPollenParams, GoogleMapsPollenResponse } from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface RawIndexInfo {
|
||||
code?: string
|
||||
displayName?: string
|
||||
value?: number
|
||||
category?: string
|
||||
indexDescription?: string
|
||||
color?: { red?: number; green?: number; blue?: number }
|
||||
}
|
||||
|
||||
const mapIndexInfo = (indexInfo: RawIndexInfo | undefined) =>
|
||||
indexInfo
|
||||
? {
|
||||
code: indexInfo.code || '',
|
||||
displayName: indexInfo.displayName || '',
|
||||
value: indexInfo.value ?? 0,
|
||||
category: indexInfo.category || '',
|
||||
indexDescription: indexInfo.indexDescription || '',
|
||||
color: {
|
||||
red: indexInfo.color?.red ?? 0,
|
||||
green: indexInfo.color?.green ?? 0,
|
||||
blue: indexInfo.color?.blue ?? 0,
|
||||
},
|
||||
}
|
||||
: null
|
||||
|
||||
export const googleMapsPollenTool: ToolConfig<GoogleMapsPollenParams, GoogleMapsPollenResponse> = {
|
||||
id: 'google_maps_pollen',
|
||||
name: 'Google Maps Pollen',
|
||||
description: 'Get a daily pollen forecast (grass, tree, weed) for a location',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Pollen API enabled',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
days: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of forecast days to return (1-5, defaults to 1)',
|
||||
},
|
||||
languageCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for the response (e.g., "en", "es")',
|
||||
},
|
||||
plantsDescription: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Include detailed plant descriptions (defaults to true)',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.005,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://pollen.googleapis.com/v1/forecast:lookup')
|
||||
url.searchParams.set('location.latitude', params.lat.toString())
|
||||
url.searchParams.set('location.longitude', params.lng.toString())
|
||||
const rawDays =
|
||||
typeof params.days === 'number' && Number.isFinite(params.days)
|
||||
? Math.trunc(params.days)
|
||||
: 1
|
||||
const days = Math.min(Math.max(rawDays, 1), 5)
|
||||
url.searchParams.set('days', days.toString())
|
||||
if (params.languageCode) {
|
||||
url.searchParams.set('languageCode', params.languageCode.trim())
|
||||
}
|
||||
if (params.plantsDescription !== undefined) {
|
||||
url.searchParams.set('plantsDescription', String(params.plantsDescription))
|
||||
}
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok || data.error) {
|
||||
throw new Error(`Pollen lookup failed: ${data.error?.message || response.statusText}`)
|
||||
}
|
||||
|
||||
const dailyInfo = (data.dailyInfo || []).map(
|
||||
(day: {
|
||||
date?: { year?: number; month?: number; day?: number }
|
||||
pollenTypeInfo?: Array<{
|
||||
code?: string
|
||||
displayName?: string
|
||||
inSeason?: boolean
|
||||
indexInfo?: RawIndexInfo
|
||||
healthRecommendations?: string[]
|
||||
}>
|
||||
plantInfo?: Array<{
|
||||
code?: string
|
||||
displayName?: string
|
||||
inSeason?: boolean
|
||||
indexInfo?: RawIndexInfo
|
||||
plantDescription?: {
|
||||
type?: string
|
||||
family?: string
|
||||
season?: string
|
||||
specialColors?: string
|
||||
specialShapes?: string
|
||||
crossReaction?: string
|
||||
picture?: string
|
||||
pictureCloseup?: string
|
||||
}
|
||||
}>
|
||||
}) => ({
|
||||
date: {
|
||||
year: day.date?.year ?? 0,
|
||||
month: day.date?.month ?? 0,
|
||||
day: day.date?.day ?? 0,
|
||||
},
|
||||
pollenTypeInfo: (day.pollenTypeInfo || []).map((type) => ({
|
||||
code: type.code || '',
|
||||
displayName: type.displayName || '',
|
||||
inSeason: type.inSeason ?? null,
|
||||
indexInfo: mapIndexInfo(type.indexInfo),
|
||||
healthRecommendations: type.healthRecommendations || [],
|
||||
})),
|
||||
plantInfo: (day.plantInfo || []).map((plant) => ({
|
||||
code: plant.code || '',
|
||||
displayName: plant.displayName || '',
|
||||
inSeason: plant.inSeason ?? null,
|
||||
indexInfo: mapIndexInfo(plant.indexInfo),
|
||||
plantDescription: plant.plantDescription
|
||||
? {
|
||||
type: plant.plantDescription.type || '',
|
||||
family: plant.plantDescription.family || '',
|
||||
season: plant.plantDescription.season || '',
|
||||
specialColors: plant.plantDescription.specialColors || '',
|
||||
specialShapes: plant.plantDescription.specialShapes || '',
|
||||
crossReaction: plant.plantDescription.crossReaction || '',
|
||||
picture: plant.plantDescription.picture || '',
|
||||
pictureCloseup: plant.plantDescription.pictureCloseup || '',
|
||||
}
|
||||
: null,
|
||||
})),
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
regionCode: data.regionCode || '',
|
||||
dailyInfo,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
regionCode: {
|
||||
type: 'string',
|
||||
description: 'Region code (ISO 3166-1 alpha-2) for the location',
|
||||
},
|
||||
dailyInfo: {
|
||||
type: 'array',
|
||||
description: 'Daily pollen forecast entries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: {
|
||||
type: 'object',
|
||||
description: 'Calendar date of the forecast entry',
|
||||
properties: {
|
||||
year: { type: 'number' },
|
||||
month: { type: 'number' },
|
||||
day: { type: 'number' },
|
||||
},
|
||||
},
|
||||
pollenTypeInfo: {
|
||||
type: 'array',
|
||||
description: 'Pollen type indices (grass, tree, weed)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string', description: 'Pollen type code (GRASS, TREE, WEED)' },
|
||||
displayName: { type: 'string', description: 'Display name' },
|
||||
inSeason: { type: 'boolean', description: 'Whether the pollen type is in season' },
|
||||
indexInfo: { type: 'object', description: 'Universal Pollen Index (UPI) info' },
|
||||
healthRecommendations: {
|
||||
type: 'array',
|
||||
description: 'Health recommendations',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plantInfo: {
|
||||
type: 'array',
|
||||
description: 'Per-plant forecast with descriptions',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string', description: 'Plant code (e.g., BIRCH, RAGWEED)' },
|
||||
displayName: { type: 'string', description: 'Display name' },
|
||||
inSeason: { type: 'boolean', description: 'Whether the plant is in season' },
|
||||
indexInfo: { type: 'object', description: 'Universal Pollen Index (UPI) info' },
|
||||
plantDescription: {
|
||||
type: 'object',
|
||||
description: 'Plant details (type, family, season, cross-reactions)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import type {
|
||||
GoogleMapsReverseGeocodeParams,
|
||||
GoogleMapsReverseGeocodeResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsReverseGeocodeTool: ToolConfig<
|
||||
GoogleMapsReverseGeocodeParams,
|
||||
GoogleMapsReverseGeocodeResponse
|
||||
> = {
|
||||
id: 'google_maps_reverse_geocode',
|
||||
name: 'Google Maps Reverse Geocode',
|
||||
description:
|
||||
'Convert geographic coordinates (latitude and longitude) into a human-readable address',
|
||||
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 coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.005,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/geocode/json')
|
||||
url.searchParams.set('latlng', `${params.lat},${params.lng}`)
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Reverse geocoding failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = data.results[0]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
formattedAddress: result.formatted_address,
|
||||
placeId: result.place_id,
|
||||
addressComponents: (result.address_components || []).map(
|
||||
(comp: { long_name: string; short_name: string; types: string[] }) => ({
|
||||
longName: comp.long_name,
|
||||
shortName: comp.short_name,
|
||||
types: comp.types,
|
||||
})
|
||||
),
|
||||
types: result.types || [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
formattedAddress: {
|
||||
type: 'string',
|
||||
description: 'The formatted address string',
|
||||
},
|
||||
placeId: {
|
||||
type: 'string',
|
||||
description: 'Google Place ID for this location',
|
||||
},
|
||||
addressComponents: {
|
||||
type: 'array',
|
||||
description: 'Detailed address components',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
longName: { type: 'string', description: 'Full name of the component' },
|
||||
shortName: { type: 'string', description: 'Abbreviated name' },
|
||||
types: { type: 'array', description: 'Component types' },
|
||||
},
|
||||
},
|
||||
},
|
||||
types: {
|
||||
type: 'array',
|
||||
description: 'Address types (e.g., street_address, route)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import type {
|
||||
GoogleMapsSnapToRoadsParams,
|
||||
GoogleMapsSnapToRoadsResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsSnapToRoadsTool: ToolConfig<
|
||||
GoogleMapsSnapToRoadsParams,
|
||||
GoogleMapsSnapToRoadsResponse
|
||||
> = {
|
||||
id: 'google_maps_snap_to_roads',
|
||||
name: 'Google Maps Snap to Roads',
|
||||
description: 'Snap GPS coordinates to the nearest road segment',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Roads API enabled',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Pipe-separated list of lat,lng coordinates (e.g., "60.170880,24.942795|60.170879,24.942796")',
|
||||
},
|
||||
interpolate: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to interpolate additional points along the road',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.01,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://roads.googleapis.com/v1/snapToRoads')
|
||||
url.searchParams.set('path', params.path.trim())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
if (params.interpolate !== undefined) {
|
||||
url.searchParams.set('interpolate', String(params.interpolate))
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`Snap to Roads failed: ${data.error.message || 'Unknown error'}`)
|
||||
}
|
||||
|
||||
const snappedPoints = (data.snappedPoints || []).map(
|
||||
(point: {
|
||||
location: { latitude: number; longitude: number }
|
||||
originalIndex?: number
|
||||
placeId: string
|
||||
}) => ({
|
||||
location: {
|
||||
lat: point.location.latitude,
|
||||
lng: point.location.longitude,
|
||||
},
|
||||
originalIndex: point.originalIndex,
|
||||
placeId: point.placeId,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
snappedPoints,
|
||||
warningMessage: data.warningMessage || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
snappedPoints: {
|
||||
type: 'array',
|
||||
description: 'Array of snapped points on roads',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
type: 'object',
|
||||
description: 'Snapped location coordinates',
|
||||
properties: {
|
||||
lat: { type: 'number', description: 'Latitude' },
|
||||
lng: { type: 'number', description: 'Longitude' },
|
||||
},
|
||||
},
|
||||
originalIndex: {
|
||||
type: 'number',
|
||||
description: 'Index in the original path (if not interpolated)',
|
||||
},
|
||||
placeId: { type: 'string', description: 'Place ID for this road segment' },
|
||||
},
|
||||
},
|
||||
},
|
||||
warningMessage: {
|
||||
type: 'string',
|
||||
description: 'Warning message if any (e.g., if points could not be snapped)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { GoogleMapsSolarParams, GoogleMapsSolarResponse } from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsSolarTool: ToolConfig<GoogleMapsSolarParams, GoogleMapsSolarResponse> = {
|
||||
id: 'google_maps_solar',
|
||||
name: 'Google Maps Solar',
|
||||
description: 'Get solar potential and panel insights for the building nearest a location',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Solar API enabled',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
requiredQuality: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Minimum imagery quality to accept (HIGH, MEDIUM, or BASE)',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.005,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://solar.googleapis.com/v1/buildingInsights:findClosest')
|
||||
url.searchParams.set('location.latitude', params.lat.toString())
|
||||
url.searchParams.set('location.longitude', params.lng.toString())
|
||||
if (params.requiredQuality) {
|
||||
url.searchParams.set('requiredQuality', params.requiredQuality)
|
||||
}
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok || data.error) {
|
||||
throw new Error(`Solar lookup failed: ${data.error?.message || response.statusText}`)
|
||||
}
|
||||
|
||||
const potential = data.solarPotential
|
||||
const solarPotential = potential
|
||||
? {
|
||||
maxArrayPanelsCount: potential.maxArrayPanelsCount ?? 0,
|
||||
maxArrayAreaMeters2: potential.maxArrayAreaMeters2 ?? 0,
|
||||
maxSunshineHoursPerYear: potential.maxSunshineHoursPerYear ?? 0,
|
||||
carbonOffsetFactorKgPerMwh: potential.carbonOffsetFactorKgPerMwh ?? 0,
|
||||
panelCapacityWatts: potential.panelCapacityWatts ?? 0,
|
||||
panelHeightMeters: potential.panelHeightMeters ?? 0,
|
||||
panelWidthMeters: potential.panelWidthMeters ?? 0,
|
||||
panelLifetimeYears: potential.panelLifetimeYears ?? 0,
|
||||
solarPanelConfigs: (potential.solarPanelConfigs || []).map(
|
||||
(config: { panelsCount?: number; yearlyEnergyDcKwh?: number }) => ({
|
||||
panelsCount: config.panelsCount ?? 0,
|
||||
yearlyEnergyDcKwh: config.yearlyEnergyDcKwh ?? 0,
|
||||
})
|
||||
),
|
||||
}
|
||||
: null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
name: data.name || '',
|
||||
center: {
|
||||
lat: data.center?.latitude ?? 0,
|
||||
lng: data.center?.longitude ?? 0,
|
||||
},
|
||||
imageryDate: data.imageryDate
|
||||
? {
|
||||
year: data.imageryDate.year ?? 0,
|
||||
month: data.imageryDate.month ?? 0,
|
||||
day: data.imageryDate.day ?? 0,
|
||||
}
|
||||
: null,
|
||||
imageryQuality: data.imageryQuality || '',
|
||||
regionCode: data.regionCode || '',
|
||||
postalCode: data.postalCode || '',
|
||||
administrativeArea: data.administrativeArea || '',
|
||||
solarPotential,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Resource name of the building (e.g., "buildings/ChIJ...")',
|
||||
},
|
||||
center: {
|
||||
type: 'object',
|
||||
description: 'Center coordinate of the building',
|
||||
properties: {
|
||||
lat: { type: 'number', description: 'Latitude' },
|
||||
lng: { type: 'number', description: 'Longitude' },
|
||||
},
|
||||
},
|
||||
imageryDate: {
|
||||
type: 'object',
|
||||
description: 'Date the underlying imagery was captured',
|
||||
},
|
||||
imageryQuality: {
|
||||
type: 'string',
|
||||
description: 'Quality of the imagery used (HIGH, MEDIUM, BASE)',
|
||||
},
|
||||
regionCode: {
|
||||
type: 'string',
|
||||
description: 'Region code (ISO 3166-1 alpha-2) for the building',
|
||||
},
|
||||
postalCode: {
|
||||
type: 'string',
|
||||
description: 'Postal code of the building',
|
||||
},
|
||||
administrativeArea: {
|
||||
type: 'string',
|
||||
description: 'Administrative area (e.g., state or province)',
|
||||
},
|
||||
solarPotential: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Solar potential: max panel count/area, sunshine hours, carbon offset, panel specs, and configs',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import type {
|
||||
GoogleMapsSpeedLimitsParams,
|
||||
GoogleMapsSpeedLimitsResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsSpeedLimitsTool: ToolConfig<
|
||||
GoogleMapsSpeedLimitsParams,
|
||||
GoogleMapsSpeedLimitsResponse
|
||||
> = {
|
||||
id: 'google_maps_speed_limits',
|
||||
name: 'Google Maps Speed Limits',
|
||||
description: 'Get speed limits for road segments. Requires either path coordinates or placeIds.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Roads API enabled',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pipe-separated list of lat,lng coordinates (required if placeIds not provided)',
|
||||
},
|
||||
placeIds: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
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: {
|
||||
url: (params) => {
|
||||
const hasPath = params.path && params.path.trim().length > 0
|
||||
const hasPlaceIds = params.placeIds && params.placeIds.length > 0
|
||||
|
||||
if (!hasPath && !hasPlaceIds) {
|
||||
throw new Error(
|
||||
'Speed Limits requires either a path (coordinates) or placeIds. Please provide at least one.'
|
||||
)
|
||||
}
|
||||
|
||||
const url = new URL('https://roads.googleapis.com/v1/speedLimits')
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
|
||||
if (hasPath) {
|
||||
url.searchParams.set('path', params.path!.trim())
|
||||
}
|
||||
|
||||
if (hasPlaceIds) {
|
||||
for (const placeId of params.placeIds!) {
|
||||
url.searchParams.append('placeId', placeId.trim())
|
||||
}
|
||||
}
|
||||
|
||||
if (params.units) {
|
||||
url.searchParams.set('units', params.units)
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`Speed Limits failed: ${data.error.message || 'Unknown error'}`)
|
||||
}
|
||||
|
||||
const speedLimits = (data.speedLimits || []).map(
|
||||
(limit: { placeId: string; speedLimit: number; units: 'KPH' | 'MPH' }) => ({
|
||||
placeId: limit.placeId,
|
||||
speedLimit: limit.speedLimit,
|
||||
units: limit.units,
|
||||
})
|
||||
)
|
||||
|
||||
const snappedPoints = (data.snappedPoints || []).map(
|
||||
(point: {
|
||||
location: { latitude: number; longitude: number }
|
||||
originalIndex?: number
|
||||
placeId: string
|
||||
}) => ({
|
||||
location: {
|
||||
lat: point.location.latitude,
|
||||
lng: point.location.longitude,
|
||||
},
|
||||
originalIndex: point.originalIndex,
|
||||
placeId: point.placeId,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
speedLimits,
|
||||
snappedPoints,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
speedLimits: {
|
||||
type: 'array',
|
||||
description: 'Array of speed limits for road segments',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
placeId: { type: 'string', description: 'Place ID for the road segment' },
|
||||
speedLimit: { type: 'number', description: 'Speed limit value' },
|
||||
units: { type: 'string', description: 'Speed limit units (KPH or MPH)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
snappedPoints: {
|
||||
type: 'array',
|
||||
description: 'Array of snapped points corresponding to the speed limits',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
type: 'object',
|
||||
description: 'Snapped location coordinates',
|
||||
properties: {
|
||||
lat: { type: 'number', description: 'Latitude' },
|
||||
lng: { type: 'number', description: 'Longitude' },
|
||||
},
|
||||
},
|
||||
originalIndex: { type: 'number', description: 'Index in the original path' },
|
||||
placeId: { type: 'string', description: 'Place ID for this road segment' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import type {
|
||||
GoogleMapsTimezoneParams,
|
||||
GoogleMapsTimezoneResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsTimezoneTool: ToolConfig<
|
||||
GoogleMapsTimezoneParams,
|
||||
GoogleMapsTimezoneResponse
|
||||
> = {
|
||||
id: 'google_maps_timezone',
|
||||
name: 'Google Maps Timezone',
|
||||
description: 'Get timezone information for 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 coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Unix timestamp to determine DST offset (defaults to current time)',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for timezone name (e.g., en, es, fr)',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.005,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/timezone/json')
|
||||
url.searchParams.set('location', `${params.lat},${params.lng}`)
|
||||
// Use provided timestamp or current time
|
||||
const timestamp = params.timestamp ?? Math.floor(Date.now() / 1000)
|
||||
url.searchParams.set('timestamp', timestamp.toString())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Timezone request failed: ${data.status} - ${data.errorMessage || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate total offset
|
||||
const totalOffsetSeconds = data.rawOffset + data.dstOffset
|
||||
const totalOffsetHours = totalOffsetSeconds / 3600
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
timeZoneId: data.timeZoneId,
|
||||
timeZoneName: data.timeZoneName,
|
||||
rawOffset: data.rawOffset,
|
||||
dstOffset: data.dstOffset,
|
||||
totalOffsetSeconds,
|
||||
totalOffsetHours,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
timeZoneId: {
|
||||
type: 'string',
|
||||
description: 'IANA timezone ID (e.g., "America/New_York", "Europe/London")',
|
||||
},
|
||||
timeZoneName: {
|
||||
type: 'string',
|
||||
description: 'Localized timezone name (e.g., "Eastern Daylight Time")',
|
||||
},
|
||||
rawOffset: {
|
||||
type: 'number',
|
||||
description: 'UTC offset in seconds (without DST)',
|
||||
},
|
||||
dstOffset: {
|
||||
type: 'number',
|
||||
description: 'Daylight Saving Time offset in seconds (0 if not in DST)',
|
||||
},
|
||||
totalOffsetSeconds: {
|
||||
type: 'number',
|
||||
description: 'Total UTC offset in seconds (rawOffset + dstOffset)',
|
||||
},
|
||||
totalOffsetHours: {
|
||||
type: 'number',
|
||||
description: 'Total UTC offset in hours (e.g., -5 for EST, -4 for EDT)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,642 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Common location type
|
||||
*/
|
||||
interface LatLng {
|
||||
lat: number
|
||||
lng: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Address component from geocoding
|
||||
*/
|
||||
interface AddressComponent {
|
||||
longName: string
|
||||
shortName: string
|
||||
types: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapped point from Roads API
|
||||
*/
|
||||
interface SnappedPoint {
|
||||
location: LatLng
|
||||
originalIndex?: number
|
||||
placeId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Speed limit info from Roads API
|
||||
*/
|
||||
interface SpeedLimit {
|
||||
placeId: string
|
||||
speedLimit: number
|
||||
units: 'KPH' | 'MPH'
|
||||
}
|
||||
|
||||
/**
|
||||
* Cell tower info for geolocation
|
||||
*/
|
||||
interface CellTower {
|
||||
cellId: number
|
||||
locationAreaCode: number
|
||||
mobileCountryCode: number
|
||||
mobileNetworkCode: number
|
||||
age?: number
|
||||
signalStrength?: number
|
||||
timingAdvance?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* WiFi access point info for geolocation
|
||||
*/
|
||||
interface WifiAccessPoint {
|
||||
macAddress: string
|
||||
signalStrength?: number
|
||||
age?: number
|
||||
channel?: number
|
||||
signalToNoiseRatio?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Air quality index info
|
||||
*/
|
||||
interface AirQualityIndex {
|
||||
code: string
|
||||
displayName: string
|
||||
aqi: number
|
||||
aqiDisplay: string
|
||||
color: {
|
||||
red: number
|
||||
green: number
|
||||
blue: number
|
||||
}
|
||||
category: string
|
||||
dominantPollutant: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Pollutant concentration info
|
||||
*/
|
||||
interface Pollutant {
|
||||
code: string
|
||||
displayName: string
|
||||
fullName: string
|
||||
concentration: {
|
||||
value: number
|
||||
units: string
|
||||
}
|
||||
additionalInfo?: {
|
||||
sources: string
|
||||
effects: string
|
||||
}
|
||||
}
|
||||
|
||||
// Geocode
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsGeocodeParams {
|
||||
apiKey: string
|
||||
address: string
|
||||
language?: string
|
||||
region?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsGeocodeResponse extends ToolResponse {
|
||||
output: {
|
||||
formattedAddress: string
|
||||
lat: number
|
||||
lng: number
|
||||
location: LatLng
|
||||
placeId: string
|
||||
addressComponents: AddressComponent[]
|
||||
locationType: string
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Reverse Geocode
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsReverseGeocodeParams {
|
||||
apiKey: string
|
||||
lat: number
|
||||
lng: number
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsReverseGeocodeResponse extends ToolResponse {
|
||||
output: {
|
||||
formattedAddress: string
|
||||
placeId: string
|
||||
addressComponents: AddressComponent[]
|
||||
types: string[]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Directions
|
||||
// ============================================================================
|
||||
|
||||
interface DirectionsStep {
|
||||
instruction: string
|
||||
distanceText: string
|
||||
distanceMeters: number
|
||||
durationText: string
|
||||
durationSeconds: number
|
||||
startLocation: LatLng
|
||||
endLocation: LatLng
|
||||
travelMode: string
|
||||
maneuver: string | null
|
||||
}
|
||||
|
||||
interface DirectionsLeg {
|
||||
startAddress: string
|
||||
endAddress: string
|
||||
startLocation: LatLng
|
||||
endLocation: LatLng
|
||||
distanceText: string
|
||||
distanceMeters: number
|
||||
durationText: string
|
||||
durationSeconds: number
|
||||
steps: DirectionsStep[]
|
||||
}
|
||||
|
||||
interface DirectionsRoute {
|
||||
summary: string
|
||||
legs: DirectionsLeg[]
|
||||
overviewPolyline: string
|
||||
warnings: string[]
|
||||
waypointOrder: number[]
|
||||
}
|
||||
|
||||
export interface GoogleMapsDirectionsParams {
|
||||
apiKey: string
|
||||
origin: string
|
||||
destination: string
|
||||
mode?: 'driving' | 'walking' | 'bicycling' | 'transit'
|
||||
avoid?: string
|
||||
waypoints?: string[]
|
||||
units?: 'metric' | 'imperial'
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsDirectionsResponse extends ToolResponse {
|
||||
output: {
|
||||
routes: DirectionsRoute[]
|
||||
distanceText: string
|
||||
distanceMeters: number
|
||||
durationText: string
|
||||
durationSeconds: number
|
||||
startAddress: string
|
||||
endAddress: string
|
||||
steps: DirectionsStep[]
|
||||
polyline: string
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Distance Matrix
|
||||
// ============================================================================
|
||||
|
||||
interface DistanceMatrixElement {
|
||||
distanceText: string
|
||||
distanceMeters: number
|
||||
durationText: string
|
||||
durationSeconds: number
|
||||
durationInTrafficText: string | null
|
||||
durationInTrafficSeconds: number | null
|
||||
status: string
|
||||
}
|
||||
|
||||
interface DistanceMatrixRow {
|
||||
elements: DistanceMatrixElement[]
|
||||
}
|
||||
|
||||
export interface GoogleMapsDistanceMatrixParams {
|
||||
apiKey: string
|
||||
origin: string
|
||||
destinations: string[]
|
||||
mode?: 'driving' | 'walking' | 'bicycling' | 'transit'
|
||||
avoid?: string
|
||||
units?: 'metric' | 'imperial'
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsDistanceMatrixResponse extends ToolResponse {
|
||||
output: {
|
||||
originAddresses: string[]
|
||||
destinationAddresses: string[]
|
||||
rows: DistanceMatrixRow[]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Places Search
|
||||
// ============================================================================
|
||||
|
||||
interface PlaceResult {
|
||||
placeId: string
|
||||
name: string
|
||||
formattedAddress: string
|
||||
lat: number
|
||||
lng: number
|
||||
types: string[]
|
||||
rating: number | null
|
||||
userRatingsTotal: number | null
|
||||
priceLevel: number | null
|
||||
openNow: boolean | null
|
||||
photoReference: string | null
|
||||
businessStatus: string | null
|
||||
}
|
||||
|
||||
export interface GoogleMapsPlacesSearchParams {
|
||||
apiKey: string
|
||||
query: string
|
||||
location?: LatLng
|
||||
radius?: number
|
||||
type?: string
|
||||
language?: string
|
||||
region?: string
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsPlacesSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
places: PlaceResult[]
|
||||
nextPageToken: string | null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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
|
||||
// ============================================================================
|
||||
|
||||
interface PlaceReview {
|
||||
authorName: string
|
||||
authorUrl: string | null
|
||||
profilePhotoUrl: string | null
|
||||
rating: number
|
||||
text: string
|
||||
time: number
|
||||
relativeTimeDescription: string
|
||||
}
|
||||
|
||||
interface PlacePhoto {
|
||||
photoReference: string
|
||||
height: number
|
||||
width: number
|
||||
htmlAttributions: string[]
|
||||
}
|
||||
|
||||
export interface GoogleMapsPlaceDetailsParams {
|
||||
apiKey: string
|
||||
placeId: string
|
||||
fields?: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsPlaceDetailsResponse extends ToolResponse {
|
||||
output: {
|
||||
placeId: string
|
||||
name: string | null
|
||||
formattedAddress: string | null
|
||||
lat: number | null
|
||||
lng: number | null
|
||||
types: string[]
|
||||
rating: number | null
|
||||
userRatingsTotal: number | null
|
||||
priceLevel: number | null
|
||||
website: string | null
|
||||
phoneNumber: string | null
|
||||
internationalPhoneNumber: string | null
|
||||
openNow: boolean | null
|
||||
weekdayText: string[]
|
||||
reviews: PlaceReview[]
|
||||
photos: PlacePhoto[]
|
||||
url: string | null
|
||||
utcOffset: number | null
|
||||
vicinity: string | null
|
||||
businessStatus: string | null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Elevation
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsElevationParams {
|
||||
apiKey: string
|
||||
lat: number
|
||||
lng: number
|
||||
}
|
||||
|
||||
export interface GoogleMapsElevationResponse extends ToolResponse {
|
||||
output: {
|
||||
elevation: number
|
||||
lat: number
|
||||
lng: number
|
||||
resolution: number | null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Timezone
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsTimezoneParams {
|
||||
apiKey: string
|
||||
lat: number
|
||||
lng: number
|
||||
timestamp?: number
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsTimezoneResponse extends ToolResponse {
|
||||
output: {
|
||||
timeZoneId: string
|
||||
timeZoneName: string
|
||||
rawOffset: number
|
||||
dstOffset: number
|
||||
totalOffsetSeconds: number
|
||||
totalOffsetHours: number
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Snap to Roads
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsSnapToRoadsParams {
|
||||
apiKey: string
|
||||
path: string
|
||||
interpolate?: boolean
|
||||
}
|
||||
|
||||
export interface GoogleMapsSnapToRoadsResponse extends ToolResponse {
|
||||
output: {
|
||||
snappedPoints: SnappedPoint[]
|
||||
warningMessage: string | null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Speed Limits
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsSpeedLimitsParams {
|
||||
apiKey: string
|
||||
path?: string
|
||||
placeIds?: string[]
|
||||
units?: 'KPH' | 'MPH'
|
||||
}
|
||||
|
||||
export interface GoogleMapsSpeedLimitsResponse extends ToolResponse {
|
||||
output: {
|
||||
speedLimits: SpeedLimit[]
|
||||
snappedPoints: SnappedPoint[]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Validate Address
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsValidateAddressParams {
|
||||
apiKey: string
|
||||
address: string
|
||||
regionCode?: string
|
||||
locality?: string
|
||||
enableUspsCass?: boolean
|
||||
}
|
||||
|
||||
export interface GoogleMapsValidateAddressResponse extends ToolResponse {
|
||||
output: {
|
||||
formattedAddress: string
|
||||
lat: number
|
||||
lng: number
|
||||
placeId: string
|
||||
addressComplete: boolean
|
||||
hasUnconfirmedComponents: boolean
|
||||
hasInferredComponents: boolean
|
||||
hasReplacedComponents: boolean
|
||||
validationGranularity: string
|
||||
geocodeGranularity: string
|
||||
addressComponents: AddressComponent[]
|
||||
missingComponentTypes: string[]
|
||||
unconfirmedComponentTypes: string[]
|
||||
unresolvedTokens: string[]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Geolocate
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsGeolocateParams {
|
||||
apiKey: string
|
||||
homeMobileCountryCode?: number
|
||||
homeMobileNetworkCode?: number
|
||||
radioType?: 'lte' | 'gsm' | 'cdma' | 'wcdma' | 'nr'
|
||||
carrier?: string
|
||||
considerIp?: boolean
|
||||
cellTowers?: CellTower[]
|
||||
wifiAccessPoints?: WifiAccessPoint[]
|
||||
}
|
||||
|
||||
export interface GoogleMapsGeolocateResponse extends ToolResponse {
|
||||
output: {
|
||||
lat: number
|
||||
lng: number
|
||||
accuracy: number
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Air Quality
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsAirQualityParams {
|
||||
apiKey: string
|
||||
lat: number
|
||||
lng: number
|
||||
languageCode?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsAirQualityResponse extends ToolResponse {
|
||||
output: {
|
||||
dateTime: string
|
||||
regionCode: string
|
||||
indexes: AirQualityIndex[]
|
||||
pollutants: Pollutant[]
|
||||
healthRecommendations: {
|
||||
generalPopulation: string
|
||||
elderly: string
|
||||
lungDiseasePopulation: string
|
||||
heartDiseasePopulation: string
|
||||
athletes: string
|
||||
pregnantWomen: string
|
||||
children: string
|
||||
} | null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Pollen
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Calendar date returned by the Pollen and Solar APIs
|
||||
*/
|
||||
interface GoogleMapsDate {
|
||||
year: number
|
||||
month: number
|
||||
day: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal pollen index info shared by pollen types and plants
|
||||
*/
|
||||
interface PollenIndexInfo {
|
||||
code: string
|
||||
displayName: string
|
||||
value: number
|
||||
category: string
|
||||
indexDescription: string
|
||||
color: {
|
||||
red: number
|
||||
green: number
|
||||
blue: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pollen type forecast (grass, tree, weed)
|
||||
*/
|
||||
interface PollenTypeInfo {
|
||||
code: string
|
||||
displayName: string
|
||||
inSeason: boolean | null
|
||||
indexInfo: PollenIndexInfo | null
|
||||
healthRecommendations: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual plant forecast with optional description
|
||||
*/
|
||||
interface PlantInfo {
|
||||
code: string
|
||||
displayName: string
|
||||
inSeason: boolean | null
|
||||
indexInfo: PollenIndexInfo | null
|
||||
plantDescription: {
|
||||
type: string
|
||||
family: string
|
||||
season: string
|
||||
specialColors: string
|
||||
specialShapes: string
|
||||
crossReaction: string
|
||||
picture: string
|
||||
pictureCloseup: string
|
||||
} | null
|
||||
}
|
||||
|
||||
interface PollenDailyInfo {
|
||||
date: GoogleMapsDate
|
||||
pollenTypeInfo: PollenTypeInfo[]
|
||||
plantInfo: PlantInfo[]
|
||||
}
|
||||
|
||||
export interface GoogleMapsPollenParams {
|
||||
apiKey: string
|
||||
lat: number
|
||||
lng: number
|
||||
days?: number
|
||||
languageCode?: string
|
||||
plantsDescription?: boolean
|
||||
}
|
||||
|
||||
export interface GoogleMapsPollenResponse extends ToolResponse {
|
||||
output: {
|
||||
regionCode: string
|
||||
dailyInfo: PollenDailyInfo[]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Solar
|
||||
// ============================================================================
|
||||
|
||||
interface SolarPanelConfig {
|
||||
panelsCount: number
|
||||
yearlyEnergyDcKwh: number
|
||||
}
|
||||
|
||||
interface SolarPotential {
|
||||
maxArrayPanelsCount: number
|
||||
maxArrayAreaMeters2: number
|
||||
maxSunshineHoursPerYear: number
|
||||
carbonOffsetFactorKgPerMwh: number
|
||||
panelCapacityWatts: number
|
||||
panelHeightMeters: number
|
||||
panelWidthMeters: number
|
||||
panelLifetimeYears: number
|
||||
solarPanelConfigs: SolarPanelConfig[]
|
||||
}
|
||||
|
||||
export interface GoogleMapsSolarParams {
|
||||
apiKey: string
|
||||
lat: number
|
||||
lng: number
|
||||
requiredQuality?: 'HIGH' | 'MEDIUM' | 'BASE'
|
||||
}
|
||||
|
||||
export interface GoogleMapsSolarResponse extends ToolResponse {
|
||||
output: {
|
||||
name: string
|
||||
center: LatLng
|
||||
imageryDate: GoogleMapsDate | null
|
||||
imageryQuality: string
|
||||
regionCode: string
|
||||
postalCode: string
|
||||
administrativeArea: string
|
||||
solarPotential: SolarPotential | null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import type {
|
||||
GoogleMapsValidateAddressParams,
|
||||
GoogleMapsValidateAddressResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsValidateAddressTool: ToolConfig<
|
||||
GoogleMapsValidateAddressParams,
|
||||
GoogleMapsValidateAddressResponse
|
||||
> = {
|
||||
id: 'google_maps_validate_address',
|
||||
name: 'Google Maps Validate Address',
|
||||
description: 'Validate and standardize a postal address',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Address Validation API enabled',
|
||||
},
|
||||
address: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The address to validate (as a single string)',
|
||||
},
|
||||
regionCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "CA")',
|
||||
},
|
||||
locality: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'City or locality name',
|
||||
},
|
||||
enableUspsCass: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Enable USPS CASS validation for US addresses',
|
||||
},
|
||||
},
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: 'GOOGLE_CLOUD_API_KEY',
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'google_cloud',
|
||||
pricing: {
|
||||
type: 'per_request',
|
||||
cost: 0.005,
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
return `https://addressvalidation.googleapis.com/v1:validateAddress?key=${params.apiKey.trim()}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: {
|
||||
address: { addressLines: string[]; regionCode?: string; locality?: string }
|
||||
enableUspsCass?: boolean
|
||||
} = {
|
||||
address: {
|
||||
addressLines: [params.address.trim()],
|
||||
},
|
||||
}
|
||||
|
||||
if (params.regionCode) {
|
||||
body.address.regionCode = params.regionCode.trim()
|
||||
}
|
||||
|
||||
if (params.locality) {
|
||||
body.address.locality = params.locality.trim()
|
||||
}
|
||||
|
||||
if (params.enableUspsCass !== undefined) {
|
||||
body.enableUspsCass = params.enableUspsCass
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`Address Validation failed: ${data.error.message || 'Unknown error'}`)
|
||||
}
|
||||
|
||||
const result = data.result
|
||||
const verdict = result?.verdict || {}
|
||||
const address = result?.address || {}
|
||||
const geocode = result?.geocode || {}
|
||||
|
||||
const addressComponents = (address.addressComponents || []).map(
|
||||
(comp: {
|
||||
componentName: { text: string; languageCode?: string }
|
||||
componentType: string
|
||||
confirmationLevel: string
|
||||
}) => ({
|
||||
longName: comp.componentName?.text || '',
|
||||
shortName: comp.componentName?.text || '',
|
||||
types: [comp.componentType],
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
formattedAddress: address.formattedAddress || '',
|
||||
lat: geocode.location?.latitude || 0,
|
||||
lng: geocode.location?.longitude || 0,
|
||||
placeId: geocode.placeId || '',
|
||||
addressComplete: verdict.addressComplete || false,
|
||||
hasUnconfirmedComponents: verdict.hasUnconfirmedComponents || false,
|
||||
hasInferredComponents: verdict.hasInferredComponents || false,
|
||||
hasReplacedComponents: verdict.hasReplacedComponents || false,
|
||||
validationGranularity: verdict.validationGranularity || '',
|
||||
geocodeGranularity: verdict.geocodeGranularity || '',
|
||||
addressComponents,
|
||||
missingComponentTypes: address.missingComponentTypes || [],
|
||||
unconfirmedComponentTypes: address.unconfirmedComponentTypes || [],
|
||||
unresolvedTokens: address.unresolvedTokens || [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
formattedAddress: {
|
||||
type: 'string',
|
||||
description: 'The standardized formatted address',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
placeId: {
|
||||
type: 'string',
|
||||
description: 'Google Place ID for this address',
|
||||
},
|
||||
addressComplete: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the address is complete and deliverable',
|
||||
},
|
||||
hasUnconfirmedComponents: {
|
||||
type: 'boolean',
|
||||
description: 'Whether some address components could not be confirmed',
|
||||
},
|
||||
hasInferredComponents: {
|
||||
type: 'boolean',
|
||||
description: 'Whether some components were inferred (not in input)',
|
||||
},
|
||||
hasReplacedComponents: {
|
||||
type: 'boolean',
|
||||
description: 'Whether some components were replaced with canonical values',
|
||||
},
|
||||
validationGranularity: {
|
||||
type: 'string',
|
||||
description: 'Granularity of validation (PREMISE, SUB_PREMISE, ROUTE, etc.)',
|
||||
},
|
||||
geocodeGranularity: {
|
||||
type: 'string',
|
||||
description: 'Granularity of the geocode result',
|
||||
},
|
||||
addressComponents: {
|
||||
type: 'array',
|
||||
description: 'Detailed address components',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
longName: { type: 'string', description: 'Full name of the component' },
|
||||
shortName: { type: 'string', description: 'Abbreviated name' },
|
||||
types: { type: 'array', description: 'Component types' },
|
||||
},
|
||||
},
|
||||
},
|
||||
missingComponentTypes: {
|
||||
type: 'array',
|
||||
description: 'Types of address components that are missing',
|
||||
},
|
||||
unconfirmedComponentTypes: {
|
||||
type: 'array',
|
||||
description: 'Types of components that could not be confirmed',
|
||||
},
|
||||
unresolvedTokens: {
|
||||
type: 'array',
|
||||
description: 'Input tokens that could not be resolved',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user