import { aiFunction, AIFunctionsProvider, assert, getEnv, omit } from '@agentic/core' import defaultKy, { type KyInstance } from 'ky' import { z } from 'zod' export namespace serper { export const API_BASE_URL = 'https://google.serper.dev' export const SearchParamsSchema = z.object({ q: z.string().describe('search query'), autocorrect: z.boolean().default(true).optional(), gl: z.string().default('us').optional(), hl: z.string().default('en').optional(), page: z.number().int().default(1).optional(), num: z .number() .int() .default(10) .optional() .describe('number of results to return') }) export type SearchParams = z.infer export const GeneralSearchSchema = SearchParamsSchema.extend({ type: z .enum(['search', 'images', 'videos', 'places', 'news', 'shopping']) .default('search') .optional() .describe('Type of Google search to perform') }) export type GeneralSearchParams = z.infer export interface SearchResponse { searchParameters: SearchParameters & { type: 'search' } organic: Organic[] answerBox?: AnswerBox knowledgeGraph?: KnowledgeGraph topStories?: TopStory[] peopleAlsoAsk?: PeopleAlsoAsk[] relatedSearches?: RelatedSearch[] } export interface SearchImagesResponse { searchParameters: SearchParameters & { type: 'images' } images: Image[] } export interface SearchVideosResponse { searchParameters: SearchParameters & { type: 'videos' } videos: Video[] } export interface SearchPlacesResponse { searchParameters: SearchParameters & { type: 'places' } places: Place[] } export interface SearchNewsResponse { searchParameters: SearchParameters & { type: 'news' } news: News[] } export interface SearchShoppingResponse { searchParameters: SearchParameters & { type: 'shopping' } shopping: Shopping[] } export type Response = | SearchResponse | SearchImagesResponse | SearchVideosResponse | SearchPlacesResponse | SearchNewsResponse | SearchShoppingResponse export interface KnowledgeGraph { title: string type: string website: string imageUrl: string description: string descriptionSource: string descriptionLink: string attributes: Record } export interface Organic { title: string link: string snippet: string position: number imageUrl?: string sitelinks?: SiteLink[] } export interface AnswerBox { snippet: string snippetHighlighted?: string[] title: string link: string date?: string position?: number } export interface SiteLink { title: string link: string } export interface PeopleAlsoAsk { question: string snippet: string title: string link: string } export interface RelatedSearch { query: string } export interface SearchParameters { q: string gl: string hl: string num: number autocorrect: boolean page: number type: string engine: string } export interface TopStory { title: string link: string source: string date: string imageUrl: string } export interface Image { title: string imageUrl: string imageWidth: number imageHeight: number thumbnailUrl: string thumbnailWidth: number thumbnailHeight: number source: string domain: string link: string googleUrl: string position: number } export interface Video { title: string link: string snippet: string date: string imageUrl: string position: number } export interface Place { position: number title: string address: string latitude: number longitude: number category: string phoneNumber?: string website: string cid: string rating?: number ratingCount?: number } export interface News { title: string link: string snippet: string date: string source: string imageUrl: string position: number } export interface Shopping { title: string source: string link: string price: string imageUrl: string delivery?: Record rating?: number ratingCount?: number offers?: string productId?: string position: number } export type ClientParams = Partial> } /** * Lightweight wrapper around Serper for Google search. * * @see https://serper.dev */ export class SerperClient extends AIFunctionsProvider { protected readonly ky: KyInstance protected readonly apiKey: string protected readonly apiBaseUrl: string protected readonly params: serper.ClientParams constructor({ apiKey = getEnv('SERPER_API_KEY'), apiBaseUrl = serper.API_BASE_URL, ky = defaultKy, ...params }: { apiKey?: string apiBaseUrl?: string ky?: KyInstance } & serper.ClientParams = {}) { assert( apiKey, 'SerperClient missing required "apiKey" (defaults to "SERPER_API_KEY")' ) super() this.apiKey = apiKey this.apiBaseUrl = apiBaseUrl this.params = params this.ky = ky.extend({ prefixUrl: this.apiBaseUrl, headers: { 'x-api-key': this.apiKey } }) } /** * Uses Google Search to return the most relevant web pages for a given query. Useful for finding up-to-date news and information about any topic. */ @aiFunction({ name: 'serper_google_search', description: 'Uses Google Search to return the most relevant web pages for a given query. Useful for finding up-to-date news and information about any topic.', inputSchema: serper.GeneralSearchSchema.pick({ q: true, num: true, type: true }) }) async search(queryOrOpts: string | serper.GeneralSearchParams) { const searchType = typeof queryOrOpts === 'string' ? 'search' : queryOrOpts.type || 'search' return this._fetch( searchType, typeof queryOrOpts === 'string' ? queryOrOpts : omit(queryOrOpts, 'type') ) } async searchImages(queryOrOpts: string | serper.SearchParams) { return this._fetch('images', queryOrOpts) } async searchVideos(queryOrOpts: string | serper.SearchParams) { return this._fetch('videos', queryOrOpts) } async searchPlaces(queryOrOpts: string | serper.SearchParams) { return this._fetch('places', queryOrOpts) } async searchNews(queryOrOpts: string | serper.SearchParams) { return this._fetch('news', queryOrOpts) } async searchProducts(queryOrOpts: string | serper.SearchParams) { return this._fetch('shopping', queryOrOpts) } protected async _fetch( endpoint: string, queryOrOpts: string | serper.SearchParams ): Promise { const params = { ...this.params, ...(typeof queryOrOpts === 'string' ? { q: queryOrOpts } : queryOrOpts) } return this.ky.post(endpoint, { json: params }).json() } }