pulse-ai-utils
Version:
Utility functions and helpers for AI-powered applications
210 lines • 9.37 kB
JavaScript
;
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