118 lines
3.6 KiB
TypeScript
118 lines
3.6 KiB
TypeScript
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);
|
|
}
|
|
}
|