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%
4.4 KB · 165 lines typescript
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      server/services/exaService.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 axios from 'axios';1819const EXA_API_KEY = process.env.EXA_API_KEY;20const EXA_BASE_URL = 'https://api.exa.ai';2122export interface ExaSearchResult {23  url: string;24  title: string;25  publishedDate?: string;26  author?: string;27  score?: number;28  text?: string;29  highlights?: string[];30  summary?: string;31}3233export interface ExaSearchResponse {34  results: ExaSearchResult[];35  autopromptString?: string;36}3738export interface ExaContentsResult {39  url: string;40  title: string;41  text?: string;42  highlights?: string[];43  summary?: string;44}4546/**47 * Search the web using Exa's semantic search48 */49export async function searchExa(50  query: string,51  options: {52    numResults?: number;53    type?: 'auto' | 'neural' | 'keyword';54    category?: string;55    includeDomains?: string[];56    excludeDomains?: string[];57    startPublishedDate?: string;58    endPublishedDate?: string;59    includeText?: boolean;60    maxCharacters?: number;61  } = {}62): Promise<ExaSearchResponse> {63  if (!EXA_API_KEY) {64    throw new Error('EXA_API_KEY not configured');65  }6667  try {68    const requestBody: any = {69      query,70      type: options.type || 'auto',71      numResults: Math.min(options.numResults || 10, 20), // Limit to 20 to avoid context overflow72    };7374    if (options.category) requestBody.category = options.category;75    if (options.includeDomains) requestBody.includeDomains = options.includeDomains;76    if (options.excludeDomains) requestBody.excludeDomains = options.excludeDomains;77    if (options.startPublishedDate) requestBody.startPublishedDate = options.startPublishedDate;78    if (options.endPublishedDate) requestBody.endPublishedDate = options.endPublishedDate;7980    // Include text content if requested81    if (options.includeText) {82      requestBody.text = {83        maxCharacters: options.maxCharacters || 2000,84        includeHtmlTags: false85      };86    }8788    const response = await axios.post(`${EXA_BASE_URL}/search`, requestBody, {89      headers: {90        'x-api-key': EXA_API_KEY,91        'Content-Type': 'application/json',92      },93      timeout: 15000,94    });9596    return response.data;97  } catch (error) {98    console.error('Exa search error:', error);99    throw new Error(`Failed to search with Exa: ${(error as Error).message}`);100  }101}102103/**104 * Get full content from specific URLs using Exa's crawling105 */106export async function getContentsExa(107  urls: string[],108  options: {109    includeText?: boolean;110    maxCharacters?: number;111    includeHighlights?: boolean;112    highlightsQuery?: string;113    numSentences?: number;114    includeSummary?: boolean;115    summaryQuery?: string;116  } = {}117): Promise<ExaContentsResult[]> {118  if (!EXA_API_KEY) {119    throw new Error('EXA_API_KEY not configured');120  }121122  try {123    const requestBody: any = {124      urls: urls.slice(0, 5), // Limit to 5 URLs to avoid context overflow125    };126127    // Include text content128    if (options.includeText !== false) {129      requestBody.text = {130        maxCharacters: options.maxCharacters || 3000,131        includeHtmlTags: false132      };133    }134135    // Include highlights136    if (options.includeHighlights) {137      requestBody.highlights = {138        query: options.highlightsQuery,139        numSentences: options.numSentences || 3,140        highlightsPerUrl: 3141      };142    }143144    // Include summary145    if (options.includeSummary && options.summaryQuery) {146      requestBody.summary = {147        query: options.summaryQuery148      };149    }150151    const response = await axios.post(`${EXA_BASE_URL}/contents`, requestBody, {152      headers: {153        'x-api-key': EXA_API_KEY,154        'Content-Type': 'application/json',155      },156      timeout: 20000,157    });158159    return response.data.results || [];160  } catch (error) {161    console.error('Exa get contents error:', error);162    throw new Error(`Failed to get contents from Exa: ${(error as Error).message}`);163  }164}165