UNPKG

pulse-ai-utils

Version:

Utility functions and helpers for AI-powered applications

210 lines 9.37 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PerplexityHelper = void 0; const openai_1 = __importDefault(require("openai")); const llm_base_1 = require("./llm-base"); const firebase_config_1 = require("../config/firebase-config"); const env_loader_1 = require("../config/env-loader"); class PerplexityHelper extends llm_base_1.LLMBase { constructor(apiKey, openaiInstance, model = PerplexityHelper.MODELS.SONAR_SMALL, cache) { const perplexityApiKey = apiKey || (0, env_loader_1.getEnvVar)('PERPLEXITY_API_KEY'); // Log diagnostic info for debugging if (!perplexityApiKey) { console.warn('[Perplexity] No API key found in environment'); // Check if we're in test mode before throwing error const isTestMode = process.env.NODE_ENV === 'test' || process.env.OPENAI_STUB_TEST === '1' || process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true'; if (!isTestMode) { throw new Error('API key is required'); } } else { console.log(`[Perplexity] API key loaded, length: ${perplexityApiKey.length}, prefix: ${perplexityApiKey.substring(0, 7)}...`); } const config = { model, apiKey: perplexityApiKey, baseURL: PerplexityHelper.BASE_URL, headers: { 'Authorization': `Bearer ${perplexityApiKey}`, 'Accept': 'application/json', } }; super(config, openaiInstance, cache); } createOpenAIInstance(config) { const perplexityApiKey = config.apiKey || this.getApiKey(); return new openai_1.default({ apiKey: perplexityApiKey, baseURL: config.baseURL, defaultHeaders: config.headers, dangerouslyAllowBrowser: false, maxRetries: 3, }); } getApiKey() { const key = (0, env_loader_1.getEnvVar)('PERPLEXITY_API_KEY'); if (key) return key; return super.getApiKey(); // This will handle test mode } getProviderName() { return 'perplexity'; } /** * Create a PerplexityHelper instance with Firebase Remote Config */ static async createWithRemoteConfig(modelOverride) { // Check for Perplexity-specific model configuration const perplexityModel = await (0, firebase_config_1.getRemoteConfigParam)('perplexityModel'); const defaultPerplexityModel = await (0, firebase_config_1.getRemoteConfigParam)('defaultPerplexityModel'); const defaultModel = perplexityModel || defaultPerplexityModel || PerplexityHelper.MODELS.SONAR_SMALL; const model = modelOverride || defaultModel; return new PerplexityHelper((0, env_loader_1.getEnvVar)('PERPLEXITY_API_KEY'), undefined, model); } /** * Get list of available Perplexity models */ static getAvailableModels() { return Object.values(PerplexityHelper.MODELS); } /** * Check if a model supports web search */ static isOnlineModel(model) { return model.includes('online'); } /** * Override generateEmbedding - Perplexity doesn't support embeddings * This will throw an error if called */ async generateEmbedding(text) { throw new Error('Perplexity does not support embedding generation. Use OpenAI or another provider for embeddings.'); } /** * Override fetchStructuredDataFromWeb to use Perplexity's native web search * Note: Perplexity models with 'online' suffix have built-in web search */ async fetchStructuredDataFromWeb(params) { const { model, prompt, userLocation, locationGranularity, systemPrompt, timeline, zodSchema, responseFormatName = 'structured_response', options = {} } = params; // Use an online model for web search const searchModel = model || this.defaultModel; if (!PerplexityHelper.isOnlineModel(searchModel)) { console.warn(`[Perplexity] Model ${searchModel} doesn't support web search. Switching to ${PerplexityHelper.MODELS.SONAR_SMALL}`); this.defaultModel = PerplexityHelper.MODELS.SONAR_SMALL; } // Build location-aware prompt const today = new Date(); const todayDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; const enhancedPrompt = `Today's date is ${todayDate}. ${prompt}; all results should be for ${locationGranularity} area. We are a local app. Ensure that they are relevant and up-to-date.`; const finalSystemPrompt = systemPrompt || 'You are a helpful assistant that searches the web for current information and provides structured JSON responses.'; try { // Perplexity doesn't support OpenAI's structured output format directly // We'll use regular chat completion and parse the response const completion = await this.openai.chat.completions.create({ model: searchModel, messages: [ { role: 'system', content: `${finalSystemPrompt}\n\nIMPORTANT: Return your response as valid JSON that matches this structure: ${JSON.stringify(zodSchema._def)}` }, { role: 'user', content: enhancedPrompt } ], temperature: 0.1, // Lower temperature for more consistent JSON }); const response = completion.choices[0]?.message?.content || '{}'; // Try to parse the JSON response let parsed; try { // Extract JSON from the response (in case it's wrapped in markdown or text) const jsonMatch = response.match(/\{[\s\S]*\}/); const jsonStr = jsonMatch ? jsonMatch[0] : response; parsed = JSON.parse(jsonStr); } catch (parseError) { console.error('[Perplexity] Failed to parse JSON response:', parseError); // Return empty structured response parsed = { [responseFormatName]: [] }; } // Validate against schema const validated = zodSchema.parse(parsed); // Cache the result (fire-and-forget) const cacheLoc = { area: locationGranularity, region: userLocation?.region, country: userLocation?.country, timeline, buttonClickCount: options.buttonClickCount ? String(options.buttonClickCount) : undefined }; // Only cache if we have meaningful results const hasData = validated && validated.data && Array.isArray(validated.data) && validated.data.length > 0; if (hasData) { this.cache.setCachedResult(prompt, cacheLoc, validated, { model: searchModel, provider: this.getProviderName(), ...options }).catch(error => { console.error('[Perplexity] Failed to cache result (non-blocking):', error.message); }); } else { console.log('[Perplexity] Skipping cache write - no meaningful data to cache'); } return validated; } catch (error) { console.error('[Perplexity] Error in fetchStructuredDataFromWeb:', error); throw error; } } /** * Get rate limit information from response headers */ getRateLimitInfo(response) { try { const headers = response?.headers || {}; const remaining = parseInt(headers['x-ratelimit-remaining'] || '0'); const reset = parseInt(headers['x-ratelimit-reset'] || '0'); const limit = parseInt(headers['x-ratelimit-limit'] || '0'); if (remaining || reset || limit) { return { remaining, reset: new Date(reset * 1000), limit }; } } catch (error) { console.warn('[Perplexity] Could not parse rate limit headers:', error); } return null; } } exports.PerplexityHelper = PerplexityHelper; PerplexityHelper.BASE_URL = 'https://api.perplexity.ai'; // Perplexity model names PerplexityHelper.MODELS = { // Online models with web search capabilities SONAR_SMALL: 'llama-3.1-sonar-small-128k-online', SONAR_LARGE: 'llama-3.1-sonar-large-128k-online', SONAR_HUGE: 'llama-3.1-sonar-huge-128k-online', // Chat models without web search CHAT_SMALL: 'llama-3.1-sonar-small-128k-chat', CHAT_LARGE: 'llama-3.1-sonar-large-128k-chat', // Legacy models (for backwards compatibility) LEGACY_7B: 'pplx-7b-online', LEGACY_70B: 'pplx-70b-online', }; //# sourceMappingURL=perplexity.js.map