feat: add Perplexity Sonar provider with OpenAI fallback
This commit is contained in:
117
src/lib/llm/providers/perplexity.ts
Normal file
117
src/lib/llm/providers/perplexity.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import type { ComparisonRequest, ComparisonResult } from "../types";
|
||||||
|
import type { SearchResult } from "./tavily";
|
||||||
|
import { generateComparisonWithResearch } from "./openai";
|
||||||
|
|
||||||
|
const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
|
||||||
|
|
||||||
|
const SYSTEM_PROMPT = `You are a research synthesis engine. Given web search results for multiple items, produce a structured JSON comparison.
|
||||||
|
|
||||||
|
You MUST respond with valid JSON matching this exact structure:
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "item name",
|
||||||
|
"description": "brief overview",
|
||||||
|
"overallScore": 7.5,
|
||||||
|
"dimensions": {
|
||||||
|
"Dimension Name": {
|
||||||
|
"score": 8,
|
||||||
|
"summary": "brief assessment",
|
||||||
|
"details": "detailed analysis",
|
||||||
|
"pros": ["pro 1"],
|
||||||
|
"cons": ["con 1"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pros": ["overall pro 1"],
|
||||||
|
"cons": ["overall con 1"],
|
||||||
|
"sources": [{ "title": "source", "url": "https://...", "snippet": "excerpt" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dimensions": ["Dimension 1", "Dimension 2"],
|
||||||
|
"summary": "comparison summary",
|
||||||
|
"recommendation": "clear recommendation"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export async function synthesizeResearch(
|
||||||
|
request: ComparisonRequest,
|
||||||
|
searchResults: Record<string, SearchResult[]>
|
||||||
|
): Promise<ComparisonResult> {
|
||||||
|
const apiKey = process.env.PERPLEXITY_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
return generateComparisonWithResearch(request, searchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allResults = Object.values(searchResults).flat();
|
||||||
|
if (allResults.length === 0) {
|
||||||
|
return generateComparisonWithResearch(request, searchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
let researchContext = "Search results for each item:\n\n";
|
||||||
|
for (const [itemName, results] of Object.entries(searchResults)) {
|
||||||
|
if (results.length === 0) continue;
|
||||||
|
researchContext += `=== ${itemName} ===\n`;
|
||||||
|
for (const r of results) {
|
||||||
|
researchContext += `- ${r.title}: ${r.content}\n Source: ${r.url}\n`;
|
||||||
|
}
|
||||||
|
researchContext += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPrompt = `Compare: ${request.items.join(", ")}
|
||||||
|
${request.query ? `Focus: ${request.query}` : ""}
|
||||||
|
${request.dimensions?.length ? `Dimensions: ${request.dimensions.join(", ")}` : ""}
|
||||||
|
|
||||||
|
${researchContext}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(PERPLEXITY_API_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "sonar",
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: SYSTEM_PROMPT },
|
||||||
|
{ role: "user", content: userPrompt },
|
||||||
|
],
|
||||||
|
temperature: 0.3,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(
|
||||||
|
`Perplexity API error: ${response.status} ${response.statusText}`
|
||||||
|
);
|
||||||
|
return generateComparisonWithResearch(request, searchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const content = data.choices?.[0]?.message?.content;
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
console.error("Empty response from Perplexity");
|
||||||
|
return generateComparisonWithResearch(request, searchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed: unknown = JSON.parse(content);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!parsed ||
|
||||||
|
typeof parsed !== "object" ||
|
||||||
|
!Array.isArray((parsed as Record<string, unknown>).items) ||
|
||||||
|
!Array.isArray((parsed as Record<string, unknown>).dimensions)
|
||||||
|
) {
|
||||||
|
console.error("Invalid structure from Perplexity, falling back to OpenAI");
|
||||||
|
return generateComparisonWithResearch(request, searchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed as ComparisonResult;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Perplexity synthesis failed, falling back to OpenAI:",
|
||||||
|
error instanceof Error ? error.message : error
|
||||||
|
);
|
||||||
|
return generateComparisonWithResearch(request, searchResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user