From 6b3a4d2bc3090d4942282dbd31fef1cc1d34068d Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 12 Feb 2026 12:31:17 -0800 Subject: [PATCH] feat(google books) Add google books integration --- apps/sim/blocks/blocks/google_books.ts | 198 +++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 13 ++ apps/sim/tools/google_books/index.ts | 3 + apps/sim/tools/google_books/types.ts | 64 ++++++ apps/sim/tools/google_books/volume_details.ts | 205 ++++++++++++++++++ apps/sim/tools/google_books/volume_search.ts | 205 ++++++++++++++++++ apps/sim/tools/registry.ts | 6 + 8 files changed, 696 insertions(+) create mode 100644 apps/sim/blocks/blocks/google_books.ts create mode 100644 apps/sim/tools/google_books/index.ts create mode 100644 apps/sim/tools/google_books/types.ts create mode 100644 apps/sim/tools/google_books/volume_details.ts create mode 100644 apps/sim/tools/google_books/volume_search.ts diff --git a/apps/sim/blocks/blocks/google_books.ts b/apps/sim/blocks/blocks/google_books.ts new file mode 100644 index 000000000..594dd008f --- /dev/null +++ b/apps/sim/blocks/blocks/google_books.ts @@ -0,0 +1,198 @@ +import { GoogleBooksIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' + +export const GoogleBooksBlock: BlockConfig = { + type: 'google_books', + name: 'Google Books', + description: 'Search and retrieve book information', + longDescription: + 'Search for books using the Google Books API. Find volumes by title, author, ISBN, or keywords, and retrieve detailed information about specific books including descriptions, ratings, and publication details.', + docsLink: 'https://docs.sim.ai/tools/google_books', + category: 'tools', + bgColor: '#4285F4', + icon: GoogleBooksIcon, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Search Volumes', id: 'volume_search' }, + { label: 'Get Volume Details', id: 'volume_details' }, + ], + value: () => 'volume_search', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + password: true, + placeholder: 'Enter your Google Books API key', + required: true, + }, + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'e.g., intitle:harry potter inauthor:rowling', + condition: { field: 'operation', value: 'volume_search' }, + required: { field: 'operation', value: 'volume_search' }, + }, + { + id: 'filter', + title: 'Filter', + type: 'dropdown', + options: [ + { label: 'None', id: '' }, + { label: 'Partial Preview', id: 'partial' }, + { label: 'Full Preview', id: 'full' }, + { label: 'Free eBooks', id: 'free-ebooks' }, + { label: 'Paid eBooks', id: 'paid-ebooks' }, + { label: 'All eBooks', id: 'ebooks' }, + ], + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'printType', + title: 'Print Type', + type: 'dropdown', + options: [ + { label: 'All', id: 'all' }, + { label: 'Books', id: 'books' }, + { label: 'Magazines', id: 'magazines' }, + ], + value: () => 'all', + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'orderBy', + title: 'Order By', + type: 'dropdown', + options: [ + { label: 'Relevance', id: 'relevance' }, + { label: 'Newest', id: 'newest' }, + ], + value: () => 'relevance', + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'maxResults', + title: 'Max Results', + type: 'short-input', + placeholder: 'Number of results (1-40)', + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'startIndex', + title: 'Start Index', + type: 'short-input', + placeholder: 'Starting index for pagination', + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'langRestrict', + title: 'Language', + type: 'short-input', + placeholder: 'ISO 639-1 code (e.g., en, es, fr)', + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'volumeId', + title: 'Volume ID', + type: 'short-input', + placeholder: 'Google Books volume ID', + condition: { field: 'operation', value: 'volume_details' }, + required: { field: 'operation', value: 'volume_details' }, + }, + { + id: 'projection', + title: 'Projection', + type: 'dropdown', + options: [ + { label: 'Full', id: 'full' }, + { label: 'Lite', id: 'lite' }, + ], + value: () => 'full', + mode: 'advanced', + }, + ], + + tools: { + access: ['google_books_volume_search', 'google_books_volume_details'], + config: { + tool: (params) => `google_books_${params.operation}`, + params: (params) => { + const { operation, ...rest } = params + + let maxResults: number | undefined + if (params.maxResults) { + maxResults = Number.parseInt(params.maxResults, 10) + if (Number.isNaN(maxResults)) { + maxResults = undefined + } + } + + let startIndex: number | undefined + if (params.startIndex) { + startIndex = Number.parseInt(params.startIndex, 10) + if (Number.isNaN(startIndex)) { + startIndex = undefined + } + } + + return { + ...rest, + maxResults, + startIndex, + filter: params.filter || undefined, + printType: params.printType || undefined, + orderBy: params.orderBy || undefined, + projection: params.projection || undefined, + } + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Google Books API key' }, + query: { type: 'string', description: 'Search query' }, + filter: { type: 'string', description: 'Filter by availability' }, + printType: { type: 'string', description: 'Print type filter' }, + orderBy: { type: 'string', description: 'Sort order' }, + maxResults: { type: 'string', description: 'Maximum number of results' }, + startIndex: { type: 'string', description: 'Starting index for pagination' }, + langRestrict: { type: 'string', description: 'Language restriction' }, + volumeId: { type: 'string', description: 'Volume ID for details' }, + projection: { type: 'string', description: 'Projection level' }, + }, + + outputs: { + totalItems: { type: 'number', description: 'Total number of matching results' }, + volumes: { type: 'json', description: 'List of matching volumes' }, + id: { type: 'string', description: 'Volume ID' }, + title: { type: 'string', description: 'Book title' }, + subtitle: { type: 'string', description: 'Book subtitle' }, + authors: { type: 'json', description: 'List of authors' }, + publisher: { type: 'string', description: 'Publisher name' }, + publishedDate: { type: 'string', description: 'Publication date' }, + description: { type: 'string', description: 'Book description' }, + pageCount: { type: 'number', description: 'Number of pages' }, + categories: { type: 'json', description: 'Book categories' }, + averageRating: { type: 'number', description: 'Average rating (1-5)' }, + ratingsCount: { type: 'number', description: 'Number of ratings' }, + language: { type: 'string', description: 'Language code' }, + previewLink: { type: 'string', description: 'Link to preview on Google Books' }, + infoLink: { type: 'string', description: 'Link to info page' }, + thumbnailUrl: { type: 'string', description: 'Book cover thumbnail URL' }, + isbn10: { type: 'string', description: 'ISBN-10 identifier' }, + isbn13: { type: 'string', description: 'ISBN-13 identifier' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 301b7b350..c5a447ce5 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -40,6 +40,7 @@ import { GitLabBlock } from '@/blocks/blocks/gitlab' import { GmailBlock, GmailV2Block } from '@/blocks/blocks/gmail' import { GoogleSearchBlock } from '@/blocks/blocks/google' import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar' +import { GoogleBooksBlock } from '@/blocks/blocks/google_books' import { GoogleDocsBlock } from '@/blocks/blocks/google_docs' import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' import { GoogleFormsBlock } from '@/blocks/blocks/google_forms' @@ -214,6 +215,7 @@ export const registry: Record = { gmail_v2: GmailV2Block, google_calendar: GoogleCalendarBlock, google_calendar_v2: GoogleCalendarV2Block, + google_books: GoogleBooksBlock, google_docs: GoogleDocsBlock, google_drive: GoogleDriveBlock, google_forms: GoogleFormsBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index f13fc8aa8..1677f5df3 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1157,6 +1157,19 @@ export function AirweaveIcon(props: SVGProps) { ) } +export function GoogleBooksIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function GoogleDocsIcon(props: SVGProps) { return ( + } + error?: { + message?: string + } +} + +export const googleBooksVolumeDetailsTool: ToolConfig< + GoogleBooksVolumeDetailsParams, + GoogleBooksVolumeDetailsResponse +> = { + id: 'google_books_volume_details', + name: 'Google Books Volume Details', + description: 'Get detailed information about a specific book volume', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Books API key', + }, + volumeId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the volume to retrieve', + }, + projection: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Projection level (full, lite)', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://www.googleapis.com/books/v1/volumes/${params.volumeId.trim()}`) + url.searchParams.set('key', params.apiKey.trim()) + + if (params.projection) { + url.searchParams.set('projection', params.projection) + } + + return url.toString() + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: GoogleBooksVolumeResponse = await response.json() + + if (data.error) { + throw new Error(`Google Books API error: ${data.error.message || 'Unknown error'}`) + } + + if (!data.volumeInfo) { + throw new Error('Volume not found') + } + + const info = data.volumeInfo + const identifiers = info.industryIdentifiers ?? [] + + return { + success: true, + output: { + id: data.id, + title: info.title ?? '', + subtitle: info.subtitle ?? null, + authors: info.authors ?? [], + publisher: info.publisher ?? null, + publishedDate: info.publishedDate ?? null, + description: info.description ?? null, + pageCount: info.pageCount ?? null, + categories: info.categories ?? [], + averageRating: info.averageRating ?? null, + ratingsCount: info.ratingsCount ?? null, + language: info.language ?? null, + previewLink: info.previewLink ?? null, + infoLink: info.infoLink ?? null, + thumbnailUrl: info.imageLinks?.thumbnail ?? info.imageLinks?.smallThumbnail ?? null, + isbn10: identifiers.find((id) => id.type === 'ISBN_10')?.identifier ?? null, + isbn13: identifiers.find((id) => id.type === 'ISBN_13')?.identifier ?? null, + }, + } + }, + + outputs: { + id: { + type: 'string', + description: 'Volume ID', + }, + title: { + type: 'string', + description: 'Book title', + }, + subtitle: { + type: 'string', + description: 'Book subtitle', + optional: true, + }, + authors: { + type: 'array', + description: 'List of authors', + }, + publisher: { + type: 'string', + description: 'Publisher name', + optional: true, + }, + publishedDate: { + type: 'string', + description: 'Publication date', + optional: true, + }, + description: { + type: 'string', + description: 'Book description', + optional: true, + }, + pageCount: { + type: 'number', + description: 'Number of pages', + optional: true, + }, + categories: { + type: 'array', + description: 'Book categories', + }, + averageRating: { + type: 'number', + description: 'Average rating (1-5)', + optional: true, + }, + ratingsCount: { + type: 'number', + description: 'Number of ratings', + optional: true, + }, + language: { + type: 'string', + description: 'Language code', + optional: true, + }, + previewLink: { + type: 'string', + description: 'Link to preview on Google Books', + optional: true, + }, + infoLink: { + type: 'string', + description: 'Link to info page', + optional: true, + }, + thumbnailUrl: { + type: 'string', + description: 'Book cover thumbnail URL', + optional: true, + }, + isbn10: { + type: 'string', + description: 'ISBN-10 identifier', + optional: true, + }, + isbn13: { + type: 'string', + description: 'ISBN-13 identifier', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_books/volume_search.ts b/apps/sim/tools/google_books/volume_search.ts new file mode 100644 index 000000000..1cdc980a9 --- /dev/null +++ b/apps/sim/tools/google_books/volume_search.ts @@ -0,0 +1,205 @@ +import type { + GoogleBooksVolumeSearchParams, + GoogleBooksVolumeSearchResponse, + VolumeInfo, +} from '@/tools/google_books/types' +import type { ToolConfig } from '@/tools/types' + +interface GoogleBooksVolumeItem { + id: string + volumeInfo: { + title?: string + subtitle?: string + authors?: string[] + publisher?: string + publishedDate?: string + description?: string + pageCount?: number + categories?: string[] + averageRating?: number + ratingsCount?: number + language?: string + previewLink?: string + infoLink?: string + imageLinks?: { + thumbnail?: string + smallThumbnail?: string + } + industryIdentifiers?: Array<{ + type: string + identifier: string + }> + } +} + +function extractVolumeInfo(item: GoogleBooksVolumeItem): VolumeInfo { + const info = item.volumeInfo + const identifiers = info.industryIdentifiers ?? [] + + return { + id: item.id, + title: info.title ?? '', + subtitle: info.subtitle ?? null, + authors: info.authors ?? [], + publisher: info.publisher ?? null, + publishedDate: info.publishedDate ?? null, + description: info.description ?? null, + pageCount: info.pageCount ?? null, + categories: info.categories ?? [], + averageRating: info.averageRating ?? null, + ratingsCount: info.ratingsCount ?? null, + language: info.language ?? null, + previewLink: info.previewLink ?? null, + infoLink: info.infoLink ?? null, + thumbnailUrl: info.imageLinks?.thumbnail ?? info.imageLinks?.smallThumbnail ?? null, + isbn10: identifiers.find((id) => id.type === 'ISBN_10')?.identifier ?? null, + isbn13: identifiers.find((id) => id.type === 'ISBN_13')?.identifier ?? null, + } +} + +export const googleBooksVolumeSearchTool: ToolConfig< + GoogleBooksVolumeSearchParams, + GoogleBooksVolumeSearchResponse +> = { + id: 'google_books_volume_search', + name: 'Google Books Volume Search', + description: 'Search for books using the Google Books API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Books API key', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Search query. Supports special keywords: intitle:, inauthor:, inpublisher:, subject:, isbn:', + }, + filter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter results by availability (partial, full, free-ebooks, paid-ebooks, ebooks)', + }, + printType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Restrict to print type (all, books, magazines)', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order (relevance, newest)', + }, + startIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Index of the first result to return (for pagination)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return (1-40)', + }, + langRestrict: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Restrict results to a specific language (ISO 639-1 code)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://www.googleapis.com/books/v1/volumes') + url.searchParams.set('q', params.query.trim()) + url.searchParams.set('key', params.apiKey.trim()) + + if (params.filter) { + url.searchParams.set('filter', params.filter) + } + if (params.printType) { + url.searchParams.set('printType', params.printType) + } + if (params.orderBy) { + url.searchParams.set('orderBy', params.orderBy) + } + if (params.startIndex !== undefined) { + url.searchParams.set('startIndex', String(params.startIndex)) + } + if (params.maxResults !== undefined) { + url.searchParams.set('maxResults', String(params.maxResults)) + } + if (params.langRestrict) { + url.searchParams.set('langRestrict', params.langRestrict) + } + + 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(`Google Books API error: ${data.error.message || 'Unknown error'}`) + } + + const items: GoogleBooksVolumeItem[] = data.items ?? [] + const volumes = items.map(extractVolumeInfo) + + return { + success: true, + output: { + totalItems: data.totalItems ?? 0, + volumes, + }, + } + }, + + outputs: { + totalItems: { + type: 'number', + description: 'Total number of matching results', + }, + volumes: { + type: 'array', + description: 'List of matching volumes', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Volume ID' }, + title: { type: 'string', description: 'Book title' }, + subtitle: { type: 'string', description: 'Book subtitle' }, + authors: { type: 'array', description: 'List of authors' }, + publisher: { type: 'string', description: 'Publisher name' }, + publishedDate: { type: 'string', description: 'Publication date' }, + description: { type: 'string', description: 'Book description' }, + pageCount: { type: 'number', description: 'Number of pages' }, + categories: { type: 'array', description: 'Book categories' }, + averageRating: { type: 'number', description: 'Average rating (1-5)' }, + ratingsCount: { type: 'number', description: 'Number of ratings' }, + language: { type: 'string', description: 'Language code' }, + previewLink: { type: 'string', description: 'Link to preview on Google Books' }, + infoLink: { type: 'string', description: 'Link to info page' }, + thumbnailUrl: { type: 'string', description: 'Book cover thumbnail URL' }, + isbn10: { type: 'string', description: 'ISBN-10 identifier' }, + isbn13: { type: 'string', description: 'ISBN-13 identifier' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 52506d744..0578fbcc0 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -549,6 +549,10 @@ import { googleCalendarUpdateV2Tool, } from '@/tools/google_calendar' import { googleDocsCreateTool, googleDocsReadTool, googleDocsWriteTool } from '@/tools/google_docs' +import { + googleBooksVolumeDetailsTool, + googleBooksVolumeSearchTool, +} from '@/tools/google_books' import { googleDriveCopyTool, googleDriveCreateFolderTool, @@ -2556,6 +2560,8 @@ export const tools: Record = { google_docs_read: googleDocsReadTool, google_docs_write: googleDocsWriteTool, google_docs_create: googleDocsCreateTool, + google_books_volume_search: googleBooksVolumeSearchTool, + google_books_volume_details: googleBooksVolumeDetailsTool, google_maps_air_quality: googleMapsAirQualityTool, google_maps_directions: googleMapsDirectionsTool, google_maps_distance_matrix: googleMapsDistanceMatrixTool,