feat(google): Maps Pollen/Solar, Custom Search expansion, and live-API fixes across Google integrations (#5113)

* feat(google): add Maps Pollen/Solar, expand Custom Search, fix Ads/Groups/Contacts/Slides

New capability:
- Google Maps: add Pollen Forecast and Solar Potential tools (API-key, google_cloud BYOK)
- Google Custom Search: add start/dateRestrict/fileType/safe/searchType/siteSearch/
  siteSearchFilter/lr/gl/sort params, htmlTitle/htmlSnippet/formattedUrl/mime/fileFormat/
  cacheId/image result fields, and nextPageStartIndex pagination

Fixes (validated against live API docs):
- Google Ads: bump all tools from sunset v19 to v24
- Google Groups: forward OAuth credential under oauthCredential (was dropping token in 11
  ops), forward all update_settings fields, JSON.stringify update_settings/add_alias bodies
- Google Contacts: include required metadata.sources[].etag in updateContact body (fixed 400)
- Google Slides: remove unsupported GIF thumbnail mimeType (API only allows PNG)
- Google Sheets: wire delete_rows/delete_sheet/delete_spreadsheet into the V2 block
- Google Custom Search: throw on API error responses instead of returning empty success;
  num optional + Number-coerced; pagemap typed unknown

* docs(google): regenerate integration docs for new and updated operations

* fix(google_maps): correct Solar requiredQuality enum to BASE

The Solar API ImageryQuality enum is HIGH/MEDIUM/BASE (+ UNSPECIFIED) per the
live docs; there is no LOW. Selecting "Low" sent requiredQuality=LOW which the
API rejects as INVALID_ARGUMENT, and the valid BASE tier was unreachable.
Replace LOW with BASE in the tool param/output descriptions, the type union,
and the block dropdown.

* fix(google_maps): guard !response.ok in Pollen/Solar; use ?? for color channels

Address Greptile review:
- Pollen and Solar transformResponse now check !response.ok || data.error
  (matches the Custom Search fix); a gateway error without an error key in the
  body no longer returns empty/zeroed output silently.
- Pollen color channels use ?? instead of || so a legitimate 0 isn't treated
  as missing (consistent with the other numeric fields in the file).

* fix(google_maps): guard against NaN days in Pollen forecast

Address Cursor Bugbot: a non-numeric `days` input parsed to NaN and was
forwarded as `days=NaN` (the tool's `?? 1` only catches undefined, not NaN),
breaking the forecast call. The block now coerces invalid input to undefined,
and the tool defaults to 1 unless `days` is a finite number.

* fix(google): clamp Pollen days to 1-5; stop forwarding stale group settings fields

Address Cursor Bugbot:
- Pollen: clamp days to the documented 1-5 range (truncating fractionals) so 0,
  negatives, or >5 can't be sent to the API.
- Google Groups update_settings: the block has no dedicated settings subblocks,
  so forwarding name/description from params could leak stale values from
  create_group/update_group and unintentionally rename the group. Forward only
  oauthCredential + groupEmail from the block (the tool's own param schema still
  exposes the settings fields for the agent path).

* fix(google_sheets): fail fast on non-numeric delete indices

Address Cursor Bugbot: delete_sheet/delete_rows parsed deleteSheetId/startIndex/
endIndex with Number.parseInt but didn't validate, so non-numeric UI input became
NaN and was forwarded (the v2 delete tools only reject null/undefined), breaking
the batchUpdate. The block now throws a clear error when any of these is not a
valid number.

* fix(google_search): clamp num to 1-10 and normalize start

Address Cursor Bugbot: num was coerced with Number() but not bounded, so values
like 11 or fractionals reached the API and failed. The tool now truncates and
clamps num to the documented 1-10 range and only sends a positive integer start,
ignoring non-numeric/out-of-range input.
This commit is contained in:
Waleed
2026-06-17 12:15:48 -07:00
committed by GitHub
parent d7fd0405f9
commit 11e23131fe
28 changed files with 1224 additions and 59 deletions
@@ -352,7 +352,7 @@ Update the settings for a Google Group including access permissions, moderation,
| `description` | string | No | The group description \(max 4096 characters\) |
| `whoCanJoin` | string | No | Who can join: ANYONE_CAN_JOIN, ALL_IN_DOMAIN_CAN_JOIN, INVITED_CAN_JOIN, CAN_REQUEST_TO_JOIN |
| `whoCanViewMembership` | string | No | Who can view membership: ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW |
| `whoCanViewGroup` | string | No | Who can view group messages: ANYONE_CAN_VIEW, ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW |
| `whoCanViewGroup` | string | No | Who can view group messages: ANYONE_CAN_VIEW, ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW, ALL_OWNERS_CAN_VIEW |
| `whoCanPostMessage` | string | No | Who can post: NONE_CAN_POST, ALL_MANAGERS_CAN_POST, ALL_MEMBERS_CAN_POST, ALL_OWNERS_CAN_POST, ALL_IN_DOMAIN_CAN_POST, ANYONE_CAN_POST |
| `allowExternalMembers` | string | No | Whether external users can be members: true or false |
| `allowWebPosting` | string | No | Whether web posting is allowed: true or false |
@@ -373,7 +373,7 @@ Update the settings for a Google Group including access permissions, moderation,
| `whoCanContactOwner` | string | No | Who can contact owner: ALL_IN_DOMAIN_CAN_CONTACT, ALL_MANAGERS_CAN_CONTACT, ALL_MEMBERS_CAN_CONTACT, ANYONE_CAN_CONTACT |
| `favoriteRepliesOnTop` | string | No | Whether favorite replies appear at top: true or false |
| `whoCanApproveMembers` | string | No | Who can approve members: ALL_OWNERS_CAN_APPROVE, ALL_MANAGERS_CAN_APPROVE, ALL_MEMBERS_CAN_APPROVE, NONE_CAN_APPROVE |
| `whoCanBanUsers` | string | No | Who can ban users: OWNERS_ONLY, OWNERS_AND_MANAGERS, NONE |
| `whoCanBanUsers` | string | No | Who can ban users: ALL_MEMBERS, OWNERS_AND_MANAGERS, OWNERS_ONLY, NONE |
| `whoCanModerateMembers` | string | No | Who can manage members: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE |
| `whoCanModerateContent` | string | No | Who can moderate content: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE |
| `whoCanAssistContent` | string | No | Who can assist with content metadata: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE |
@@ -326,6 +326,43 @@ Search for places using a text query
| ↳ `businessStatus` | string | Business status |
| `nextPageToken` | string | Token for fetching the next page of results |
### `google_maps_pollen`
Get a daily pollen forecast (grass, tree, weed) for a location
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Google Maps API key with Pollen API enabled |
| `lat` | number | Yes | Latitude coordinate |
| `lng` | number | Yes | Longitude coordinate |
| `days` | number | No | Number of forecast days to return \(1-5, defaults to 1\) |
| `languageCode` | string | No | Language code for the response \(e.g., "en", "es"\) |
| `plantsDescription` | boolean | No | Include detailed plant descriptions \(defaults to true\) |
| `pricing` | per_request | No | No description |
| `rateLimit` | string | No | No description |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `regionCode` | string | Region code \(ISO 3166-1 alpha-2\) for the location |
| `dailyInfo` | array | Daily pollen forecast entries |
| ↳ `date` | object | Calendar date of the forecast entry |
| ↳ `pollenTypeInfo` | array | Pollen type indices \(grass, tree, weed\) |
| ↳ `code` | string | Pollen type code \(GRASS, TREE, WEED\) |
| ↳ `displayName` | string | Display name |
| ↳ `inSeason` | boolean | Whether the pollen type is in season |
| ↳ `indexInfo` | object | Universal Pollen Index \(UPI\) info |
| ↳ `healthRecommendations` | array | Health recommendations |
| ↳ `plantInfo` | array | Per-plant forecast with descriptions |
| ↳ `code` | string | Plant code \(e.g., BIRCH, RAGWEED\) |
| ↳ `displayName` | string | Display name |
| ↳ `inSeason` | boolean | Whether the plant is in season |
| ↳ `indexInfo` | object | Universal Pollen Index \(UPI\) info |
| ↳ `plantDescription` | object | Plant details \(type, family, season, cross-reactions\) |
### `google_maps_reverse_geocode`
Convert geographic coordinates (latitude and longitude) into a human-readable address
@@ -379,6 +416,36 @@ Snap GPS coordinates to the nearest road segment
| ↳ `placeId` | string | Place ID for this road segment |
| `warningMessage` | string | Warning message if any \(e.g., if points could not be snapped\) |
### `google_maps_solar`
Get solar potential and panel insights for the building nearest a location
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Google Maps API key with Solar API enabled |
| `lat` | number | Yes | Latitude coordinate |
| `lng` | number | Yes | Longitude coordinate |
| `requiredQuality` | string | No | Minimum imagery quality to accept \(HIGH, MEDIUM, or BASE\) |
| `pricing` | per_request | No | No description |
| `rateLimit` | string | No | No description |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `name` | string | Resource name of the building \(e.g., "buildings/ChIJ..."\) |
| `center` | object | Center coordinate of the building |
| ↳ `lat` | number | Latitude |
| ↳ `lng` | number | Longitude |
| `imageryDate` | object | Date the underlying imagery was captured |
| `imageryQuality` | string | Quality of the imagery used \(HIGH, MEDIUM, BASE\) |
| `regionCode` | string | Region code \(ISO 3166-1 alpha-2\) for the building |
| `postalCode` | string | Postal code of the building |
| `administrativeArea` | string | Administrative area \(e.g., state or province\) |
| `solarPotential` | object | Solar potential: max panel count/area, sunshine hours, carbon offset, panel specs, and configs |
### `google_maps_speed_limits`
Get speed limits for road segments. Requires either path coordinates or placeIds.
@@ -39,7 +39,17 @@ Search the web with the Custom Search API
| --------- | ---- | -------- | ----------- |
| `query` | string | Yes | The search query to execute |
| `searchEngineId` | string | Yes | Custom Search Engine ID |
| `num` | string | No | Number of results to return \(default: 10, max: 10\) |
| `num` | string | No | Number of results to return \(1-10, default 10\) |
| `start` | number | No | Index of the first result \(1-based, for pagination; start + num must be <= 100\) |
| `dateRestrict` | string | No | Restrict results by recency: d\[n\] days, w\[n\] weeks, m\[n\] months, y\[n\] years |
| `fileType` | string | No | Restrict to a file extension \(e.g., pdf, doc\) |
| `safe` | string | No | SafeSearch level: "active" or "off" \(default off\) |
| `searchType` | string | No | Set to "image" to perform an image search |
| `siteSearch` | string | No | A site to include or exclude from results |
| `siteSearchFilter` | string | No | Whether to include \("i"\) or exclude \("e"\) the siteSearch site |
| `lr` | string | No | Restrict to a language, e.g. "lang_en" |
| `gl` | string | No | Two-letter country code to boost geographically relevant results |
| `sort` | string | No | Sort expression, e.g. "date" |
| `apiKey` | string | Yes | Google API key |
#### Output
@@ -48,14 +58,29 @@ Search the web with the Custom Search API
| --------- | ---- | ----------- |
| `items` | array | Array of search results from Google |
| ↳ `title` | string | Title of the search result |
| ↳ `htmlTitle` | string | Title of the search result with HTML markup |
| ↳ `link` | string | URL of the search result |
| ↳ `snippet` | string | Snippet or description of the search result |
| ↳ `displayLink` | string | Display URL \(abbreviated form\) |
| ↳ `snippet` | string | Snippet or description of the search result |
| ↳ `htmlSnippet` | string | Snippet of the search result with HTML markup |
| ↳ `formattedUrl` | string | Display URL shown beneath the result |
| ↳ `mime` | string | MIME type of the result |
| ↳ `fileFormat` | string | File format of the result |
| ↳ `cacheId` | string | ID of Google's cached version |
| ↳ `pagemap` | object | PageMap information for the result \(structured data\) |
| ↳ `image` | object | Image metadata \(present when searchType is image\) |
| ↳ `contextLink` | string | URL of the page hosting the image |
| ↳ `height` | number | Image height in pixels |
| ↳ `width` | number | Image width in pixels |
| ↳ `byteSize` | number | Image file size in bytes |
| ↳ `thumbnailLink` | string | Thumbnail image URL |
| ↳ `thumbnailHeight` | number | Thumbnail height in pixels |
| ↳ `thumbnailWidth` | number | Thumbnail width in pixels |
| `searchInformation` | object | Information about the search query and results |
| ↳ `totalResults` | string | Total number of search results available |
| ↳ `searchTime` | number | Time taken to perform the search in seconds |
| ↳ `formattedSearchTime` | string | Formatted search time for display |
| ↳ `formattedTotalResults` | string | Formatted total results count for display |
| `nextPageStartIndex` | number | Start index for the next page of results \(null if no further results\) |
@@ -320,6 +320,68 @@ Copy a sheet from one spreadsheet to another
| `destinationSpreadsheetId` | string | The ID of the destination spreadsheet |
| `destinationSpreadsheetUrl` | string | URL to the destination spreadsheet |
### `google_sheets_delete_rows`
Delete rows from a sheet in a Google Sheets spreadsheet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `spreadsheetId` | string | Yes | Google Sheets spreadsheet ID |
| `sheetId` | number | Yes | The numeric ID of the sheet/tab \(not the sheet name\). Use Get Spreadsheet to find sheet IDs. |
| `startIndex` | number | Yes | The start row index \(0-based, inclusive\) of the rows to delete |
| `endIndex` | number | Yes | The end row index \(0-based, exclusive\) of the rows to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `spreadsheetId` | string | Google Sheets spreadsheet ID |
| `sheetId` | number | The numeric ID of the sheet |
| `deletedRowRange` | string | Description of the deleted row range |
| `metadata` | json | Spreadsheet metadata including ID and URL |
| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID |
| ↳ `spreadsheetUrl` | string | Spreadsheet URL |
### `google_sheets_delete_sheet`
Delete a sheet/tab from a Google Sheets spreadsheet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `spreadsheetId` | string | Yes | Google Sheets spreadsheet ID |
| `sheetId` | number | Yes | The numeric ID of the sheet/tab to delete \(not the sheet name\). Use Get Spreadsheet to find sheet IDs. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `spreadsheetId` | string | Google Sheets spreadsheet ID |
| `deletedSheetId` | number | The numeric ID of the deleted sheet |
| `metadata` | json | Spreadsheet metadata including ID and URL |
| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID |
| ↳ `spreadsheetUrl` | string | Spreadsheet URL |
### `google_sheets_delete_spreadsheet`
Permanently delete a Google Sheets spreadsheet using the Google Drive API
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `spreadsheetId` | string | Yes | The ID of the Google Sheets spreadsheet to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `spreadsheetId` | string | The ID of the deleted spreadsheet |
| `deleted` | boolean | Whether the spreadsheet was successfully deleted |
## Triggers
@@ -193,7 +193,7 @@ Generate a thumbnail image of a specific slide in a Google Slides presentation
| `presentationId` | string | Yes | Google Slides presentation ID |
| `pageObjectId` | string | Yes | The object ID of the slide/page to get a thumbnail for |
| `thumbnailSize` | string | No | The size of the thumbnail: SMALL \(200px\), MEDIUM \(800px\), or LARGE \(1600px\). Defaults to MEDIUM. |
| `mimeType` | string | No | The MIME type of the thumbnail image: PNG or GIF. Defaults to PNG. |
| `mimeType` | string | No | The MIME type of the thumbnail image: PNG. Defaults to PNG. |
#### Output
+105 -4
View File
@@ -61,8 +61,88 @@ Return ONLY the search query - no explanations, no quotes around the whole thing
id: 'num',
title: 'Number of Results',
type: 'short-input',
placeholder: '10',
required: true,
placeholder: '10 (1-10)',
mode: 'advanced',
},
{
id: 'start',
title: 'Start Index',
type: 'short-input',
placeholder: '1 (for pagination; start + num <= 100)',
mode: 'advanced',
},
{
id: 'searchType',
title: 'Search Type',
type: 'dropdown',
options: [
{ label: 'Web', id: '' },
{ label: 'Image', id: 'image' },
],
mode: 'advanced',
},
{
id: 'dateRestrict',
title: 'Date Restrict',
type: 'short-input',
placeholder: 'e.g., d7, w2, m1, y1',
mode: 'advanced',
},
{
id: 'fileType',
title: 'File Type',
type: 'short-input',
placeholder: 'e.g., pdf, doc',
mode: 'advanced',
},
{
id: 'safe',
title: 'SafeSearch',
type: 'dropdown',
options: [
{ label: 'Off', id: '' },
{ label: 'Active', id: 'active' },
],
mode: 'advanced',
},
{
id: 'siteSearch',
title: 'Site Search',
type: 'short-input',
placeholder: 'Domain to include or exclude (e.g., wikipedia.org)',
mode: 'advanced',
},
{
id: 'siteSearchFilter',
title: 'Site Search Filter',
type: 'dropdown',
options: [
{ label: 'Include', id: 'i' },
{ label: 'Exclude', id: 'e' },
],
condition: { field: 'siteSearch', value: '', not: true },
mode: 'advanced',
},
{
id: 'lr',
title: 'Language Restrict',
type: 'short-input',
placeholder: 'e.g., lang_en',
mode: 'advanced',
},
{
id: 'gl',
title: 'Country (geolocation)',
type: 'short-input',
placeholder: 'Two-letter country code (e.g., us)',
mode: 'advanced',
},
{
id: 'sort',
title: 'Sort',
type: 'short-input',
placeholder: 'e.g., date',
mode: 'advanced',
},
],
@@ -74,7 +154,17 @@ Return ONLY the search query - no explanations, no quotes around the whole thing
query: params.query,
apiKey: params.apiKey,
searchEngineId: params.searchEngineId,
num: params.num || undefined,
num: params.num ? Number(params.num) : undefined,
start: params.start ? Number(params.start) : undefined,
dateRestrict: params.dateRestrict || undefined,
fileType: params.fileType || undefined,
safe: params.safe || undefined,
searchType: params.searchType || undefined,
siteSearch: params.siteSearch || undefined,
siteSearchFilter: params.siteSearch ? params.siteSearchFilter || undefined : undefined,
lr: params.lr || undefined,
gl: params.gl || undefined,
sort: params.sort || undefined,
}),
},
},
@@ -83,12 +173,23 @@ Return ONLY the search query - no explanations, no quotes around the whole thing
query: { type: 'string', description: 'Search query terms' },
apiKey: { type: 'string', description: 'Google API key' },
searchEngineId: { type: 'string', description: 'Custom search engine ID' },
num: { type: 'string', description: 'Number of results' },
num: { type: 'string', description: 'Number of results (1-10)' },
start: { type: 'string', description: 'Start index for pagination (1-based)' },
dateRestrict: { type: 'string', description: 'Restrict by recency (d/w/m/y notation)' },
fileType: { type: 'string', description: 'Restrict to a file extension' },
safe: { type: 'string', description: 'SafeSearch level (active/off)' },
searchType: { type: 'string', description: 'Search type (image for image search)' },
siteSearch: { type: 'string', description: 'Site to include or exclude' },
siteSearchFilter: { type: 'string', description: 'Include (i) or exclude (e) the site' },
lr: { type: 'string', description: 'Language restriction (e.g., lang_en)' },
gl: { type: 'string', description: 'Country geolocation code' },
sort: { type: 'string', description: 'Sort expression (e.g., date)' },
},
outputs: {
items: { type: 'json', description: 'Search result items' },
searchInformation: { type: 'json', description: 'Search metadata' },
nextPageStartIndex: { type: 'number', description: 'Start index for the next page of results' },
},
}
+10 -10
View File
@@ -337,19 +337,19 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
case 'get_group':
case 'delete_group':
return {
credential: oauthCredential,
oauthCredential,
groupKey: rest.groupKey,
}
case 'create_group':
return {
credential: oauthCredential,
oauthCredential,
email: rest.email,
name: rest.name,
description: rest.description,
}
case 'update_group':
return {
credential: oauthCredential,
oauthCredential,
groupKey: rest.groupKey,
name: rest.newName,
email: rest.newEmail,
@@ -357,7 +357,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
}
case 'list_members':
return {
credential: oauthCredential,
oauthCredential,
groupKey: rest.groupKey,
maxResults: rest.maxResults ? Number(rest.maxResults) : undefined,
roles: rest.roles,
@@ -365,38 +365,38 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
case 'get_member':
case 'remove_member':
return {
credential: oauthCredential,
oauthCredential,
groupKey: rest.groupKey,
memberKey: rest.memberKey,
}
case 'add_member':
return {
credential: oauthCredential,
oauthCredential,
groupKey: rest.groupKey,
email: rest.memberEmail,
role: rest.role,
}
case 'update_member':
return {
credential: oauthCredential,
oauthCredential,
groupKey: rest.groupKey,
memberKey: rest.memberKey,
role: rest.role,
}
case 'has_member':
return {
credential: oauthCredential,
oauthCredential,
groupKey: rest.groupKey,
memberKey: rest.memberKey,
}
case 'list_aliases':
return {
credential: oauthCredential,
oauthCredential,
groupKey: rest.groupKey,
}
case 'add_alias':
return {
credential: oauthCredential,
oauthCredential,
groupKey: rest.groupKey,
alias: rest.alias,
}
+75 -8
View File
@@ -34,6 +34,8 @@ export const GoogleMapsBlock: BlockConfig = {
{ label: 'Validate Address', id: 'validate_address' },
{ label: 'Geolocate (WiFi/Cell)', id: 'geolocate' },
{ label: 'Air Quality', id: 'air_quality' },
{ label: 'Pollen Forecast', id: 'pollen' },
{ label: 'Solar Potential', id: 'solar' },
],
value: () => 'geocode',
},
@@ -354,23 +356,53 @@ export const GoogleMapsBlock: BlockConfig = {
title: 'Latitude',
type: 'short-input',
placeholder: '37.4224764',
condition: { field: 'operation', value: 'air_quality' },
required: { field: 'operation', value: 'air_quality' },
condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] },
required: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] },
},
{
id: 'aqLongitude',
title: 'Longitude',
type: 'short-input',
placeholder: '-122.0842499',
condition: { field: 'operation', value: 'air_quality' },
required: { field: 'operation', value: 'air_quality' },
condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] },
required: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] },
},
{
id: 'languageCode',
title: 'Language Code',
type: 'short-input',
placeholder: 'Language code (e.g., en, es)',
condition: { field: 'operation', value: 'air_quality' },
condition: { field: 'operation', value: ['air_quality', 'pollen'] },
mode: 'advanced',
},
{
id: 'days',
title: 'Forecast Days',
type: 'short-input',
placeholder: 'Number of days (1-5, defaults to 1)',
condition: { field: 'operation', value: 'pollen' },
mode: 'advanced',
},
{
id: 'plantsDescription',
title: 'Include Plant Descriptions',
type: 'switch',
condition: { field: 'operation', value: 'pollen' },
mode: 'advanced',
},
{
id: 'requiredQuality',
title: 'Minimum Imagery Quality',
type: 'dropdown',
options: [
{ label: 'Any', id: '' },
{ label: 'High', id: 'HIGH' },
{ label: 'Medium', id: 'MEDIUM' },
{ label: 'Base', id: 'BASE' },
],
condition: { field: 'operation', value: 'solar' },
mode: 'advanced',
},
@@ -401,8 +433,10 @@ export const GoogleMapsBlock: BlockConfig = {
'google_maps_geolocate',
'google_maps_place_details',
'google_maps_places_search',
'google_maps_pollen',
'google_maps_reverse_geocode',
'google_maps_snap_to_roads',
'google_maps_solar',
'google_maps_speed_limits',
'google_maps_timezone',
'google_maps_validate_address',
@@ -503,6 +537,18 @@ export const GoogleMapsBlock: BlockConfig = {
considerIp = params.considerIp === 'true' || params.considerIp === true
}
let days: number | undefined
if (params.days) {
const parsedDays = Number.parseInt(params.days, 10)
days = Number.isNaN(parsedDays) ? undefined : parsedDays
}
let plantsDescription: boolean | undefined
if (params.plantsDescription !== undefined) {
plantsDescription =
params.plantsDescription === 'true' || params.plantsDescription === true
}
return {
...rest,
address,
@@ -519,6 +565,9 @@ export const GoogleMapsBlock: BlockConfig = {
interpolate,
enableUspsCass,
considerIp,
days,
plantsDescription,
requiredQuality: params.requiredQuality || undefined,
type: params.placeType || undefined,
avoid: params.avoid || undefined,
radioType: params.radioType || undefined,
@@ -561,9 +610,12 @@ export const GoogleMapsBlock: BlockConfig = {
carrier: { type: 'string', description: 'Carrier name' },
wifiAccessPoints: { type: 'string', description: 'WiFi access points JSON' },
cellTowers: { type: 'string', description: 'Cell towers JSON' },
aqLatitude: { type: 'string', description: 'Latitude for air quality' },
aqLongitude: { type: 'string', description: 'Longitude for air quality' },
languageCode: { type: 'string', description: 'Language code for air quality' },
aqLatitude: { type: 'string', description: 'Latitude for air quality, pollen, or solar' },
aqLongitude: { type: 'string', description: 'Longitude for air quality, pollen, or solar' },
languageCode: { type: 'string', description: 'Language code for air quality or pollen' },
days: { type: 'string', description: 'Number of pollen forecast days (1-5)' },
plantsDescription: { type: 'boolean', description: 'Include detailed plant descriptions' },
requiredQuality: { type: 'string', description: 'Minimum solar imagery quality' },
},
outputs: {
@@ -636,6 +688,21 @@ export const GoogleMapsBlock: BlockConfig = {
indexes: { type: 'json', description: 'Air quality indexes' },
pollutants: { type: 'json', description: 'Pollutant concentrations' },
healthRecommendations: { type: 'json', description: 'Health recommendations' },
dailyInfo: { type: 'json', description: 'Daily pollen forecast (grass, tree, weed, plants)' },
center: { type: 'json', description: 'Center coordinate of the solar building' },
imageryDate: { type: 'json', description: 'Date the solar imagery was captured' },
imageryQuality: { type: 'string', description: 'Quality of the solar imagery used' },
postalCode: { type: 'string', description: 'Postal code of the solar building' },
administrativeArea: {
type: 'string',
description: 'Administrative area of the solar building',
},
solarPotential: {
type: 'json',
description: 'Solar potential, panel specs, and configurations',
},
},
}
+129 -4
View File
@@ -326,6 +326,9 @@ export const GoogleSheetsV2Block: BlockConfig<GoogleSheetsV2Response> = {
{ label: 'Batch Update', id: 'batch_update' },
{ label: 'Batch Clear', id: 'batch_clear' },
{ label: 'Copy Sheet', id: 'copy_sheet' },
{ label: 'Delete Rows', id: 'delete_rows' },
{ label: 'Delete Sheet', id: 'delete_sheet' },
{ label: 'Delete Spreadsheet', id: 'delete_spreadsheet' },
],
value: () => 'read',
},
@@ -720,6 +723,32 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
condition: { field: 'operation', value: 'copy_sheet' },
required: true,
},
// Delete Rows / Delete Sheet Fields
{
id: 'deleteSheetId',
title: 'Sheet ID',
type: 'short-input',
placeholder: 'Numeric ID of the sheet/tab (use Get Spreadsheet Info to find IDs)',
condition: { field: 'operation', value: ['delete_rows', 'delete_sheet'] },
required: true,
},
// Delete Rows Fields
{
id: 'startIndex',
title: 'Start Row Index',
type: 'short-input',
placeholder: '0-based, inclusive (e.g., 0 for the first row)',
condition: { field: 'operation', value: 'delete_rows' },
required: true,
},
{
id: 'endIndex',
title: 'End Row Index',
type: 'short-input',
placeholder: '0-based, exclusive (e.g., 5 to delete through the fifth row)',
condition: { field: 'operation', value: 'delete_rows' },
required: true,
},
...getTrigger('google_sheets_poller').subBlocks,
],
tools: {
@@ -735,6 +764,9 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
'google_sheets_batch_update_v2',
'google_sheets_batch_clear_v2',
'google_sheets_copy_sheet_v2',
'google_sheets_delete_rows_v2',
'google_sheets_delete_sheet_v2',
'google_sheets_delete_spreadsheet_v2',
],
config: {
tool: createVersionedToolSelector({
@@ -762,6 +794,12 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
return 'google_sheets_batch_clear'
case 'copy_sheet':
return 'google_sheets_copy_sheet'
case 'delete_rows':
return 'google_sheets_delete_rows'
case 'delete_sheet':
return 'google_sheets_delete_sheet'
case 'delete_spreadsheet':
return 'google_sheets_delete_spreadsheet'
default:
throw new Error(`Invalid Google Sheets operation: ${params.operation}`)
}
@@ -782,6 +820,9 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
batchData,
sheetId,
destinationSpreadsheetId,
deleteSheetId,
startIndex,
endIndex,
filterColumn,
filterValue,
filterMatchType,
@@ -857,6 +898,48 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
}
}
// Handle delete_spreadsheet operation
if (operation === 'delete_spreadsheet') {
return {
spreadsheetId: effectiveSpreadsheetId,
oauthCredential,
}
}
// Handle delete_sheet operation
if (operation === 'delete_sheet') {
const parsedSheetId = Number.parseInt(deleteSheetId as string, 10)
if (Number.isNaN(parsedSheetId)) {
throw new Error('Sheet ID must be a valid number')
}
return {
spreadsheetId: effectiveSpreadsheetId,
sheetId: parsedSheetId,
oauthCredential,
}
}
// Handle delete_rows operation
if (operation === 'delete_rows') {
const parsedSheetId = Number.parseInt(deleteSheetId as string, 10)
const parsedStartIndex = Number.parseInt(startIndex as string, 10)
const parsedEndIndex = Number.parseInt(endIndex as string, 10)
if (
Number.isNaN(parsedSheetId) ||
Number.isNaN(parsedStartIndex) ||
Number.isNaN(parsedEndIndex)
) {
throw new Error('Sheet ID, start index, and end index must be valid numbers')
}
return {
spreadsheetId: effectiveSpreadsheetId,
sheetId: parsedSheetId,
startIndex: parsedStartIndex,
endIndex: parsedEndIndex,
oauthCredential,
}
}
// Handle read/write/update/append/clear operations (require sheet name)
const effectiveSheetName = sheetName ? String(sheetName).trim() : ''
@@ -900,6 +983,18 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
type: 'string',
description: 'Destination spreadsheet ID for copy',
},
deleteSheetId: {
type: 'string',
description: 'Numeric sheet ID for delete rows/sheet operations',
},
startIndex: {
type: 'string',
description: 'Start row index (0-based, inclusive) for delete rows operation',
},
endIndex: {
type: 'string',
description: 'End row index (0-based, exclusive) for delete rows operation',
},
filterColumn: {
type: 'string',
description: 'Column header name to filter the read rows on (within the read range)',
@@ -972,7 +1067,16 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
description: 'Spreadsheet ID',
condition: {
field: 'operation',
value: ['get_info', 'create', 'batch_get', 'batch_update', 'batch_clear'],
value: [
'get_info',
'create',
'batch_get',
'batch_update',
'batch_clear',
'delete_rows',
'delete_sheet',
'delete_spreadsheet',
],
},
},
title: {
@@ -1038,11 +1142,12 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
description: 'Array of ranges that were cleared',
condition: { field: 'operation', value: 'batch_clear' },
},
// Copy Sheet outputs
// Copy Sheet / Delete Rows outputs
sheetId: {
type: 'number',
description: 'ID of the copied sheet in the destination',
condition: { field: 'operation', value: 'copy_sheet' },
description:
'ID of the copied sheet in the destination, or the sheet the rows were deleted from',
condition: { field: 'operation', value: ['copy_sheet', 'delete_rows'] },
},
index: {
type: 'number',
@@ -1064,6 +1169,24 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
description: 'URL of the destination spreadsheet',
condition: { field: 'operation', value: 'copy_sheet' },
},
// Delete Rows outputs
deletedRowRange: {
type: 'string',
description: 'Description of the deleted row range',
condition: { field: 'operation', value: 'delete_rows' },
},
// Delete Sheet outputs
deletedSheetId: {
type: 'number',
description: 'The numeric ID of the deleted sheet',
condition: { field: 'operation', value: 'delete_sheet' },
},
// Delete Spreadsheet outputs
deleted: {
type: 'boolean',
description: 'Whether the spreadsheet was successfully deleted',
condition: { field: 'operation', value: 'delete_spreadsheet' },
},
// Common metadata
metadata: {
type: 'json',
@@ -1079,6 +1202,8 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
'batch_get',
'batch_update',
'batch_clear',
'delete_rows',
'delete_sheet',
],
},
},
+2 -5
View File
@@ -521,10 +521,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
id: 'mimeType',
title: 'Image Format',
type: 'dropdown',
options: [
{ label: 'PNG', id: 'PNG' },
{ label: 'GIF', id: 'GIF' },
],
options: [{ label: 'PNG', id: 'PNG' }],
condition: { field: 'operation', value: 'get_thumbnail' },
value: () => 'PNG',
},
@@ -3052,7 +3049,7 @@ Return ONLY the text content - no explanations, no markdown formatting markers,
// Get thumbnail operation
thumbnailPageId: { type: 'string', description: 'Slide object ID for thumbnail' },
thumbnailSize: { type: 'string', description: 'Thumbnail size' },
mimeType: { type: 'string', description: 'Image format (PNG or GIF)' },
mimeType: { type: 'string', description: 'Image format (PNG)' },
// Get page operation
getPageObjectId: { type: 'string', description: 'Page/slide object ID to retrieve' },
// Delete object operation
+22 -2
View File
@@ -6408,9 +6408,17 @@
{
"name": "Air Quality",
"description": "Get current air quality data for a location"
},
{
"name": "Pollen Forecast",
"description": "Get a daily pollen forecast (grass, tree, weed) for a location"
},
{
"name": "Solar Potential",
"description": "Get solar potential and panel insights for the building nearest a location"
}
],
"operationCount": 13,
"operationCount": 15,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
@@ -6551,9 +6559,21 @@
{
"name": "Copy Sheet",
"description": "Copy a sheet from one spreadsheet to another"
},
{
"name": "Delete Rows",
"description": "Delete rows from a sheet in a Google Sheets spreadsheet"
},
{
"name": "Delete Sheet",
"description": "Delete a sheet/tab from a Google Sheets spreadsheet"
},
{
"name": "Delete Spreadsheet",
"description": "Permanently delete a Google Sheets spreadsheet using the Google Drive API"
}
],
"operationCount": 11,
"operationCount": 14,
"triggers": [
{
"id": "google_sheets_poller",
+107 -4
View File
@@ -28,7 +28,68 @@ export const searchTool: ToolConfig<GoogleSearchParams, GoogleSearchResponse> =
type: 'string', // Treated as string for compatibility with tool interfaces
required: false,
visibility: 'user-only',
description: 'Number of results to return (default: 10, max: 10)',
description: 'Number of results to return (1-10, default 10)',
},
start: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Index of the first result (1-based, for pagination; start + num must be <= 100)',
},
dateRestrict: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict results by recency: d[n] days, w[n] weeks, m[n] months, y[n] years',
},
fileType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict to a file extension (e.g., pdf, doc)',
},
safe: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'SafeSearch level: "active" or "off" (default off)',
},
searchType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Set to "image" to perform an image search',
},
siteSearch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'A site to include or exclude from results',
},
siteSearchFilter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Whether to include ("i") or exclude ("e") the siteSearch site',
},
lr: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict to a language, e.g. "lang_en"',
},
gl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Two-letter country code to boost geographically relevant results',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort expression, e.g. "date"',
},
apiKey: {
type: 'string',
@@ -48,9 +109,41 @@ export const searchTool: ToolConfig<GoogleSearchParams, GoogleSearchResponse> =
searchParams.append('q', params.query)
searchParams.append('cx', params.searchEngineId)
// Add optional parameter
if (params.num) {
searchParams.append('num', params.num.toString())
// Add optional parameters
const num = Math.trunc(Number(params.num))
if (Number.isFinite(num) && num > 0) {
searchParams.append('num', Math.min(num, 10).toString())
}
const start = Math.trunc(Number(params.start))
if (Number.isFinite(start) && start > 0) {
searchParams.append('start', start.toString())
}
if (params.dateRestrict) {
searchParams.append('dateRestrict', params.dateRestrict)
}
if (params.fileType) {
searchParams.append('fileType', params.fileType)
}
if (params.safe) {
searchParams.append('safe', params.safe)
}
if (params.searchType) {
searchParams.append('searchType', params.searchType)
}
if (params.siteSearch) {
searchParams.append('siteSearch', params.siteSearch)
if (params.siteSearchFilter) {
searchParams.append('siteSearchFilter', params.siteSearchFilter)
}
}
if (params.lr) {
searchParams.append('lr', params.lr)
}
if (params.gl) {
searchParams.append('gl', params.gl)
}
if (params.sort) {
searchParams.append('sort', params.sort)
}
return `${baseUrl}?${searchParams.toString()}`
@@ -64,6 +157,10 @@ export const searchTool: ToolConfig<GoogleSearchParams, GoogleSearchResponse> =
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok || data.error) {
throw new Error(`Google Search failed: ${data.error?.message || response.statusText}`)
}
return {
success: true,
output: {
@@ -74,6 +171,7 @@ export const searchTool: ToolConfig<GoogleSearchParams, GoogleSearchResponse> =
formattedSearchTime: '0',
formattedTotalResults: '0',
},
nextPageStartIndex: data.queries?.nextPage?.[0]?.startIndex ?? null,
},
}
},
@@ -92,5 +190,10 @@ export const searchTool: ToolConfig<GoogleSearchParams, GoogleSearchResponse> =
description: 'Information about the search query and results',
properties: GOOGLE_SEARCH_INFORMATION_OUTPUT_PROPERTIES,
},
nextPageStartIndex: {
type: 'number',
description: 'Start index for the next page of results (null if no further results)',
optional: true,
},
},
}
+61 -3
View File
@@ -11,14 +11,46 @@ import type { OutputProperty, ToolResponse } from '@/tools/types'
*/
export const GOOGLE_SEARCH_RESULT_OUTPUT_PROPERTIES = {
title: { type: 'string', description: 'Title of the search result' },
htmlTitle: {
type: 'string',
description: 'Title of the search result with HTML markup',
optional: true,
},
link: { type: 'string', description: 'URL of the search result' },
snippet: { type: 'string', description: 'Snippet or description of the search result' },
displayLink: { type: 'string', description: 'Display URL (abbreviated form)', optional: true },
snippet: { type: 'string', description: 'Snippet or description of the search result' },
htmlSnippet: {
type: 'string',
description: 'Snippet of the search result with HTML markup',
optional: true,
},
formattedUrl: {
type: 'string',
description: 'Display URL shown beneath the result',
optional: true,
},
mime: { type: 'string', description: 'MIME type of the result', optional: true },
fileFormat: { type: 'string', description: 'File format of the result', optional: true },
cacheId: { type: 'string', description: "ID of Google's cached version", optional: true },
pagemap: {
type: 'object',
description: 'PageMap information for the result (structured data)',
optional: true,
},
image: {
type: 'object',
description: 'Image metadata (present when searchType is image)',
optional: true,
properties: {
contextLink: { type: 'string', description: 'URL of the page hosting the image' },
height: { type: 'number', description: 'Image height in pixels' },
width: { type: 'number', description: 'Image width in pixels' },
byteSize: { type: 'number', description: 'Image file size in bytes' },
thumbnailLink: { type: 'string', description: 'Thumbnail image URL' },
thumbnailHeight: { type: 'number', description: 'Thumbnail height in pixels' },
thumbnailWidth: { type: 'number', description: 'Thumbnail width in pixels' },
},
},
} as const satisfies Record<string, OutputProperty>
/**
@@ -58,16 +90,41 @@ export interface GoogleSearchParams {
apiKey: string
searchEngineId: string
num?: number | string
start?: number | string
dateRestrict?: string
fileType?: string
safe?: string
searchType?: string
siteSearch?: string
siteSearchFilter?: string
lr?: string
gl?: string
sort?: string
}
export interface GoogleSearchResponse extends ToolResponse {
output: {
items: Array<{
title: string
htmlTitle?: string
link: string
snippet: string
displayLink?: string
pagemap?: Record<string, any>
snippet: string
htmlSnippet?: string
formattedUrl?: string
mime?: string
fileFormat?: string
cacheId?: string
pagemap?: Record<string, unknown>
image?: {
contextLink?: string
height?: number
width?: number
byteSize?: number
thumbnailLink?: string
thumbnailHeight?: number
thumbnailWidth?: number
}
}>
searchInformation: {
totalResults: string
@@ -75,5 +132,6 @@ export interface GoogleSearchResponse extends ToolResponse {
formattedSearchTime: string
formattedTotalResults: string
}
nextPageStartIndex: number | null
}
}
+1 -1
View File
@@ -86,7 +86,7 @@ export const googleAdsAdPerformanceTool: ToolConfig<
request: {
url: (params) => {
const customerId = validateNumericId(params.customerId, 'customerId')
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
return `https://googleads.googleapis.com/v24/customers/${customerId}/googleAds:search`
},
method: 'POST',
headers: (params) => {
@@ -74,7 +74,7 @@ export const googleAdsCampaignPerformanceTool: ToolConfig<
request: {
url: (params) => {
const customerId = validateNumericId(params.customerId, 'customerId')
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
return `https://googleads.googleapis.com/v24/customers/${customerId}/googleAds:search`
},
method: 'POST',
headers: (params) => {
+1 -1
View File
@@ -67,7 +67,7 @@ export const googleAdsListAdGroupsTool: ToolConfig<
request: {
url: (params) => {
const customerId = validateNumericId(params.customerId, 'customerId')
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
return `https://googleads.googleapis.com/v24/customers/${customerId}/googleAds:search`
},
method: 'POST',
headers: (params) => {
+1 -1
View File
@@ -61,7 +61,7 @@ export const googleAdsListCampaignsTool: ToolConfig<
request: {
url: (params) => {
const customerId = validateNumericId(params.customerId, 'customerId')
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
return `https://googleads.googleapis.com/v24/customers/${customerId}/googleAds:search`
},
method: 'POST',
headers: (params) => {
+1 -1
View File
@@ -34,7 +34,7 @@ export const googleAdsListCustomersTool: ToolConfig<
},
request: {
url: 'https://googleads.googleapis.com/v19/customers:listAccessibleCustomers',
url: 'https://googleads.googleapis.com/v24/customers:listAccessibleCustomers',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
+1 -1
View File
@@ -55,7 +55,7 @@ export const googleAdsSearchTool: ToolConfig<GoogleAdsSearchParams, GoogleAdsSea
request: {
url: (params) => {
const customerId = validateNumericId(params.customerId, 'customerId')
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
return `https://googleads.googleapis.com/v24/customers/${customerId}/googleAds:search`
},
method: 'POST',
headers: (params) => {
+1
View File
@@ -121,6 +121,7 @@ export const updateTool: ToolConfig<GoogleContactsUpdateParams, GoogleContactsUp
body: (params: GoogleContactsUpdateParams) => {
const person: Record<string, any> = {
etag: params.etag,
metadata: { sources: [{ type: 'CONTACT', etag: params.etag }] },
}
if (params.givenName || params.familyName) {
+4 -3
View File
@@ -47,9 +47,10 @@ export const addAliasTool: ToolConfig<GoogleGroupsAddAliasParams, GoogleGroupsAd
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
alias: params.alias.trim(),
}),
body: (params) =>
JSON.stringify({
alias: params.alias.trim(),
}),
},
transformResponse: async (response) => {
@@ -63,7 +63,7 @@ export const updateSettingsTool: ToolConfig<
required: false,
visibility: 'user-or-llm',
description:
'Who can view group messages: ANYONE_CAN_VIEW, ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW',
'Who can view group messages: ANYONE_CAN_VIEW, ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW, ALL_OWNERS_CAN_VIEW',
},
whoCanPostMessage: {
type: 'string',
@@ -194,7 +194,7 @@ export const updateSettingsTool: ToolConfig<
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Who can ban users: OWNERS_ONLY, OWNERS_AND_MANAGERS, NONE',
description: 'Who can ban users: ALL_MEMBERS, OWNERS_AND_MANAGERS, OWNERS_ONLY, NONE',
},
whoCanModerateMembers: {
type: 'string',
@@ -297,7 +297,7 @@ export const updateSettingsTool: ToolConfig<
if (params.whoCanDiscoverGroup !== undefined)
body.whoCanDiscoverGroup = params.whoCanDiscoverGroup
if (params.defaultSender !== undefined) body.defaultSender = params.defaultSender
return body
return JSON.stringify(body)
},
},
+4
View File
@@ -6,8 +6,10 @@ import { googleMapsGeocodeTool } from '@/tools/google_maps/geocode'
import { googleMapsGeolocateTool } from '@/tools/google_maps/geolocate'
import { googleMapsPlaceDetailsTool } from '@/tools/google_maps/place_details'
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'
@@ -21,8 +23,10 @@ export {
googleMapsGeolocateTool,
googleMapsPlaceDetailsTool,
googleMapsPlacesSearchTool,
googleMapsPollenTool,
googleMapsReverseGeocodeTool,
googleMapsSnapToRoadsTool,
googleMapsSolarTool,
googleMapsSpeedLimitsTool,
googleMapsTimezoneTool,
googleMapsValidateAddressTool,
+249
View File
@@ -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)',
},
},
},
},
},
},
},
},
}
+158
View File
@@ -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',
},
},
}
+123
View File
@@ -479,3 +479,126 @@ export interface GoogleMapsAirQualityResponse extends ToolResponse {
} | 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
}
}
@@ -30,7 +30,7 @@ interface GetThumbnailResponse {
const THUMBNAIL_SIZES = ['SMALL', 'MEDIUM', 'LARGE'] as const
// Available MIME types for thumbnails
const MIME_TYPES = ['PNG', 'GIF'] as const
const MIME_TYPES = ['PNG'] as const
export const getThumbnailTool: ToolConfig<GetThumbnailParams, GetThumbnailResponse> = {
id: 'google_slides_get_thumbnail',
@@ -73,7 +73,7 @@ export const getThumbnailTool: ToolConfig<GetThumbnailParams, GetThumbnailRespon
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The MIME type of the thumbnail image: PNG or GIF. Defaults to PNG.',
description: 'The MIME type of the thumbnail image: PNG. Defaults to PNG.',
},
},
+4
View File
@@ -1277,8 +1277,10 @@ import {
googleMapsGeolocateTool,
googleMapsPlaceDetailsTool,
googleMapsPlacesSearchTool,
googleMapsPollenTool,
googleMapsReverseGeocodeTool,
googleMapsSnapToRoadsTool,
googleMapsSolarTool,
googleMapsSpeedLimitsTool,
googleMapsTimezoneTool,
googleMapsValidateAddressTool,
@@ -5349,8 +5351,10 @@ export const tools: Record<string, ToolConfig> = {
google_maps_geolocate: googleMapsGeolocateTool,
google_maps_place_details: googleMapsPlaceDetailsTool,
google_maps_places_search: googleMapsPlacesSearchTool,
google_maps_pollen: googleMapsPollenTool,
google_maps_reverse_geocode: googleMapsReverseGeocodeTool,
google_maps_snap_to_roads: googleMapsSnapToRoadsTool,
google_maps_solar: googleMapsSolarTool,
google_maps_speed_limits: googleMapsSpeedLimitsTool,
google_maps_timezone: googleMapsTimezoneTool,
google_maps_validate_address: googleMapsValidateAddressTool,