SPB Git

spb/vquant Public MIT

VibeQuant — AI-powered institutional-grade financial intelligence platform.

TypeScript 84.3% Python 11.7% JavaScript 1.6% CSS 1.5% HTML 0.7%
15.8 KB · 562 lines typescript
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      server/services/serpapiService.ts6 *7 *  Author:    Simon-Pierre Boucher8 *  Contact:   contact@spboucher.ai9 *  Website:   https://www.spboucher.ai10 *  Demo:      https://www.vquant.ai11 *  License:   MIT (see LICENSE)12 *13 *  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import { getJson } from 'serpapi';1819const SERPAPI_API_KEY = process.env.SERPAPI_API_KEY;2021export interface ImageResult {22  title: string;23  url: string;24  thumbnail: string;25  source: string;26  width?: number;27  height?: number;28}2930export interface MapDirections {31  from: string;32  to: string;33  distance: string;34  duration: string;35  steps: Array<{36    instruction: string;37    distance: string;38    duration: string;39  }>;40  mapUrl: string;41}4243export interface ShoppingProduct {44  title: string;45  price: string;46  source: string;47  link: string;48  thumbnail: string;49  rating?: number;50  reviews?: number;51  delivery?: string;52}5354export async function searchImages(query: string, limit: number = 10): Promise<ImageResult[]> {55  if (!SERPAPI_API_KEY) {56    throw new Error('SERPAPI_API_KEY not configured');57  }5859  try {60    const response = await getJson({61      engine: 'google_images',62      q: query,63      api_key: SERPAPI_API_KEY,64      num: Math.min(limit, 20),65    });6667    const images: ImageResult[] = (response.images_results || []).slice(0, limit).map((img: any) => ({68      title: img.title || '',69      url: img.original || img.link || '',70      thumbnail: img.thumbnail || img.original || '',71      source: img.source || '',72      width: img.original_width,73      height: img.original_height,74    }));7576    return images;77  } catch (error) {78    console.error('SerpAPI image search error:', error);79    throw new Error(`Failed to search images: ${(error as Error).message}`);80  }81}8283export async function getMapDirections(from: string, to: string): Promise<MapDirections> {84  if (!SERPAPI_API_KEY) {85    throw new Error('SERPAPI_API_KEY not configured');86  }8788  try {89    const response = await getJson({90      engine: 'google_maps_directions',91      start_addr: from,92      end_addr: to,93      api_key: SERPAPI_API_KEY,94    });9596    const directions = response.directions || [];97    const route = directions[0] || {};98    const placesInfo = response.places_info || [];99    100    const steps = (route.steps || []).map((step: any) => ({101      instruction: step.instruction || '',102      distance: formatDistance(step.distance),103      duration: step.duration || '',104    }));105106    const mapUrl = `https://www.google.com/maps/dir/?api=1&origin=${encodeURIComponent(from)}&destination=${encodeURIComponent(to)}`;107108    return {109      from: placesInfo[0]?.address || from,110      to: placesInfo[1]?.address || to,111      distance: formatDistance(route.distance),112      duration: route.duration || 'N/A',113      steps: steps.slice(0, 10),114      mapUrl,115    };116  } catch (error) {117    console.error('SerpAPI directions error:', error);118    throw new Error(`Failed to get directions: ${(error as Error).message}`);119  }120}121122function formatDistance(meters: number | undefined): string {123  if (!meters) return 'N/A';124  if (meters >= 1000) {125    return `${(meters / 1000).toFixed(1)} km`;126  }127  return `${meters} m`;128}129130export async function searchGoogleShopping(query: string, limit: number = 10): Promise<ShoppingProduct[]> {131  if (!SERPAPI_API_KEY) {132    throw new Error('SERPAPI_API_KEY not configured');133  }134135  try {136    const response = await getJson({137      engine: 'google_shopping',138      q: query,139      api_key: SERPAPI_API_KEY,140      num: Math.min(limit, 20),141    });142143    const products: ShoppingProduct[] = (response.shopping_results || []).slice(0, limit).map((product: any) => ({144      title: product.title || '',145      price: product.price || product.extracted_price || 'N/A',146      source: product.source || '',147      link: product.link || '',148      thumbnail: product.thumbnail || '',149      rating: product.rating ? parseFloat(product.rating) : undefined,150      reviews: product.reviews ? parseInt(product.reviews) : undefined,151      delivery: product.delivery || undefined,152    }));153154    return products;155  } catch (error) {156    console.error('SerpAPI shopping search error:', error);157    throw new Error(`Failed to search products: ${(error as Error).message}`);158  }159}160161export interface FinanceData {162  symbol: string;163  price: string;164  currency: string;165  changePercent?: string;166  marketCap?: string;167  priceHistory?: Array<{168    date: string;169    price: number;170  }>;171}172173export async function searchFinance(query: string): Promise<FinanceData> {174  if (!SERPAPI_API_KEY) {175    throw new Error('SERPAPI_API_KEY not configured');176  }177178  try {179    const response = await getJson({180      engine: 'google_finance',181      q: query,182      api_key: SERPAPI_API_KEY,183    });184185    const summary = response.summary || {};186    const graph = response.graph || [];187    188    const extractedPrice = summary.extracted_price;189    const price = extractedPrice ? `${extractedPrice}` : (summary.price || 'N/A');190    const currency = summary.currency || 'USD';191    const symbol = summary.stock || query;192    193    let changePercent = summary.price_movement?.percentage;194    if (changePercent) {195      changePercent = changePercent > 0 ? `+${changePercent.toFixed(2)}%` : `${changePercent.toFixed(2)}%`;196    }197    198    const priceHistory = graph.slice(-30).map((point: any) => ({199      date: point.date || '',200      price: parseFloat(point.price || 0),201    })).filter((p: any) => p.price > 0 && p.date);202203    return {204      symbol,205      price,206      currency,207      changePercent,208      marketCap: summary.market_cap || undefined,209      priceHistory: priceHistory.length > 0 ? priceHistory : undefined,210    };211  } catch (error) {212    console.error('SerpAPI finance search error:', error);213    throw new Error(`Failed to search finance data: ${(error as Error).message}`);214  }215}216217export interface JobPosting {218  title: string;219  company: string;220  location: string;221  description: string;222  link: string;223  thumbnail?: string;224  postedAt?: string;225  salary?: string;226}227228export async function searchJobs(query: string, location?: string, limit: number = 10): Promise<JobPosting[]> {229  if (!SERPAPI_API_KEY) {230    throw new Error('SERPAPI_API_KEY not configured');231  }232233  try {234    const response = await getJson({235      engine: 'google_jobs',236      q: query,237      location: location || '',238      api_key: SERPAPI_API_KEY,239    });240241    const jobs: JobPosting[] = (response.jobs_results || []).slice(0, limit).map((job: any) => ({242      title: job.title || '',243      company: job.company_name || '',244      location: job.location || '',245      description: job.description || '',246      link: job.share_link || job.apply_link || '',247      thumbnail: job.thumbnail || undefined,248      postedAt: job.detected_extensions?.posted_at || undefined,249      salary: job.detected_extensions?.salary || undefined,250    }));251252    return jobs;253  } catch (error) {254    console.error('SerpAPI jobs search error:', error);255    throw new Error(`Failed to search jobs: ${(error as Error).message}`);256  }257}258259export interface FlightOption {260  airline: string;261  flightNumber?: string;262  departure: {263    airport: string;264    time: string;265  };266  arrival: {267    airport: string;268    time: string;269  };270  duration: string;271  price: string;272  stops?: number;273  link?: string;274}275276export async function searchFlights(277  departureId: string,278  arrivalId: string,279  outboundDate: string,280  returnDate?: string281): Promise<FlightOption[]> {282  if (!SERPAPI_API_KEY) {283    throw new Error('SERPAPI_API_KEY not configured');284  }285286  try {287    const response = await getJson({288      engine: 'google_flights',289      departure_id: departureId,290      arrival_id: arrivalId,291      outbound_date: outboundDate,292      return_date: returnDate,293      api_key: SERPAPI_API_KEY,294      currency: 'USD',295    });296297    const flights: FlightOption[] = (response.best_flights || response.other_flights || []).slice(0, 10).map((flight: any) => {298      const firstFlight = flight.flights?.[0] || {};299      return {300        airline: firstFlight.airline || 'Unknown',301        flightNumber: firstFlight.flight_number || undefined,302        departure: {303          airport: firstFlight.departure_airport?.id || departureId,304          time: firstFlight.departure_airport?.time || '',305        },306        arrival: {307          airport: firstFlight.arrival_airport?.id || arrivalId,308          time: firstFlight.arrival_airport?.time || '',309        },310        duration: flight.total_duration || 'N/A',311        price: flight.price || 'N/A',312        stops: (flight.flights?.length || 1) - 1,313        link: flight.booking_token ? `https://www.google.com/travel/flights` : undefined,314      };315    });316317    return flights;318  } catch (error) {319    console.error('SerpAPI flights search error:', error);320    throw new Error(`Failed to search flights: ${(error as Error).message}`);321  }322}323324export interface HotelOption {325  name: string;326  rating?: number;327  reviews?: number;328  price: string;329  thumbnail?: string;330  link: string;331  address?: string;332  amenities?: string[];333}334335export async function searchHotels(336  query: string,337  checkInDate?: string,338  checkOutDate?: string,339  limit: number = 10340): Promise<HotelOption[]> {341  if (!SERPAPI_API_KEY) {342    throw new Error('SERPAPI_API_KEY not configured');343  }344345  if (checkInDate && !checkOutDate) {346    throw new Error('check_out_date is required when check_in_date is provided');347  }348349  try {350    const params: any = {351      engine: 'google_hotels',352      q: query,353      api_key: SERPAPI_API_KEY,354      currency: 'USD',355    };356357    if (checkInDate && checkOutDate) {358      params.check_in_date = checkInDate;359      params.check_out_date = checkOutDate;360    }361362    const response = await getJson(params);363364    const hotels: HotelOption[] = (response.properties || []).slice(0, limit).map((hotel: any) => ({365      name: hotel.name || '',366      rating: hotel.overall_rating ? parseFloat(hotel.overall_rating) : undefined,367      reviews: hotel.reviews ? parseInt(hotel.reviews) : undefined,368      price: hotel.rate_per_night?.lowest || hotel.total_rate?.lowest || 'N/A',369      thumbnail: hotel.images?.[0]?.thumbnail || undefined,370      link: hotel.link || '',371      address: hotel.description || undefined,372      amenities: hotel.amenities?.slice(0, 5) || undefined,373    }));374375    return hotels;376  } catch (error) {377    console.error('SerpAPI hotels search error:', error);378    throw new Error(`Failed to search hotels: ${(error as Error).message}`);379  }380}381382export interface VideoResult {383  title: string;384  link: string;385  thumbnail: string;386  channel: string;387  duration?: string;388  publishedDate?: string;389  views?: string;390}391392export async function searchVideos(query: string, limit: number = 10): Promise<VideoResult[]> {393  if (!SERPAPI_API_KEY) {394    throw new Error('SERPAPI_API_KEY not configured');395  }396397  try {398    const response = await getJson({399      engine: 'google_videos',400      q: query,401      api_key: SERPAPI_API_KEY,402    });403404    const videos: VideoResult[] = (response.video_results || []).slice(0, limit).map((video: any) => ({405      title: video.title || '',406      link: video.link || '',407      thumbnail: video.thumbnail || '',408      channel: video.channel || video.source || '',409      duration: video.duration || undefined,410      publishedDate: video.published_date || video.date || undefined,411      views: video.views || undefined,412    }));413414    return videos;415  } catch (error) {416    console.error('SerpAPI videos search error:', error);417    throw new Error(`Failed to search videos: ${(error as Error).message}`);418  }419}420421export interface GoogleTrendData {422  query: string;423  interest: Array<{424    date: string;425    value: number;426  }>;427  relatedQueries?: Array<{428    query: string;429    value: number;430  }>;431  risingQueries?: Array<{432    query: string;433    value: string;434  }>;435}436437export async function searchGoogleTrends(query: string): Promise<GoogleTrendData> {438  if (!SERPAPI_API_KEY) {439    throw new Error('SERPAPI_API_KEY not configured');440  }441442  try {443    const response = await getJson({444      engine: 'google_trends',445      q: query,446      api_key: SERPAPI_API_KEY,447      data_type: 'TIMESERIES',448    });449450    const interestOverTime = response.interest_over_time?.timeline_data || [];451    const relatedQueries = response.related_queries?.top || [];452    const risingQueries = response.related_queries?.rising || [];453454    return {455      query,456      interest: interestOverTime.slice(0, 30).map((point: any) => ({457        date: point.date || '',458        value: point.values?.[0]?.extracted_value || 0,459      })),460      relatedQueries: relatedQueries.slice(0, 10).map((q: any) => ({461        query: q.query || '',462        value: q.value || 0,463      })),464      risingQueries: risingQueries.slice(0, 10).map((q: any) => ({465        query: q.query || '',466        value: q.value || 'N/A',467      })),468    };469  } catch (error) {470    console.error('SerpAPI Google Trends search error:', error);471    throw new Error(`Failed to search Google Trends: ${(error as Error).message}`);472  }473}474475export interface ScholarArticle {476  title: string;477  link: string;478  authors?: string;479  publication: string;480  year?: string;481  citedBy?: number;482  snippet: string;483  pdfLink?: string;484}485486export async function searchGoogleScholar(query: string, limit: number = 10): Promise<ScholarArticle[]> {487  if (!SERPAPI_API_KEY) {488    throw new Error('SERPAPI_API_KEY not configured');489  }490491  try {492    const response = await getJson({493      engine: 'google_scholar',494      q: query,495      api_key: SERPAPI_API_KEY,496      num: Math.min(limit, 20),497    });498499    const articles: ScholarArticle[] = (response.organic_results || []).slice(0, limit).map((article: any) => ({500      title: article.title || '',501      link: article.link || '',502      authors: article.publication_info?.authors?.map((a: any) => a.name).join(', ') || undefined,503      publication: article.publication_info?.summary || '',504      year: article.publication_info?.summary?.match(/\d{4}/)?.[0] || undefined,505      citedBy: article.inline_links?.cited_by?.total ? parseInt(article.inline_links.cited_by.total) : undefined,506      snippet: article.snippet || '',507      pdfLink: article.resources?.find((r: any) => r.file_format === 'PDF')?.link || undefined,508    }));509510    return articles;511  } catch (error) {512    console.error('SerpAPI Google Scholar search error:', error);513    throw new Error(`Failed to search Google Scholar: ${(error as Error).message}`);514  }515}516517export interface AppStoreApp {518  title: string;519  link: string;520  appId: string;521  developer: string;522  rating?: number;523  ratingCount?: number;524  price: string;525  thumbnail?: string;526  description: string;527  category?: string;528}529530export async function searchAppStore(query: string, limit: number = 10): Promise<AppStoreApp[]> {531  if (!SERPAPI_API_KEY) {532    throw new Error('SERPAPI_API_KEY not configured');533  }534535  try {536    const response = await getJson({537      engine: 'apple_app_store',538      term: query,539      api_key: SERPAPI_API_KEY,540      num: Math.min(limit, 20),541    });542543    const apps: AppStoreApp[] = (response.organic_results || []).slice(0, limit).map((app: any) => ({544      title: app.title || '',545      link: app.link || '',546      appId: app.product_id || '',547      developer: app.developer || '',548      rating: app.rating ? parseFloat(app.rating) : undefined,549      ratingCount: app.reviews ? parseInt(app.reviews) : undefined,550      price: app.price || 'Free',551      thumbnail: app.thumbnail || undefined,552      description: app.description || '',553      category: app.category || undefined,554    }));555556    return apps;557  } catch (error) {558    console.error('SerpAPI App Store search error:', error);559    throw new Error(`Failed to search App Store: ${(error as Error).message}`);560  }561}562