148 lines
5.0 KiB
TypeScript
148 lines
5.0 KiB
TypeScript
import OpenAI from "openai";
|
|
import type {
|
|
ComparisonRequest,
|
|
ComparisonResult,
|
|
DimensionResult,
|
|
ItemResearch,
|
|
} from "../types";
|
|
|
|
let _client: OpenAI | null = null;
|
|
|
|
function getClient(): OpenAI {
|
|
if (!_client) {
|
|
_client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
}
|
|
return _client;
|
|
}
|
|
|
|
const SYSTEM_PROMPT = `You are an expert research analyst. Your job is to compare items across multiple dimensions and produce structured, insightful comparison data.
|
|
|
|
When given a list of items to compare:
|
|
1. Identify 5-8 relevant comparison dimensions (e.g., price, performance, ease of use, ecosystem, community support, scalability, documentation, etc.)
|
|
2. Research each item across each dimension
|
|
3. Score each item 1-10 per dimension (10 = best)
|
|
4. Generate pros and cons lists for each item
|
|
5. Write a concise summary comparison
|
|
6. Provide a clear recommendation
|
|
|
|
You MUST respond with valid JSON matching this exact structure:
|
|
{
|
|
"items": [
|
|
{
|
|
"name": "item name",
|
|
"description": "brief overview of this item in context of the comparison",
|
|
"overallScore": 7.5,
|
|
"dimensions": {
|
|
"Dimension Name": {
|
|
"score": 8,
|
|
"summary": "brief assessment",
|
|
"details": "detailed analysis with specific data points",
|
|
"pros": ["pro 1", "pro 2"],
|
|
"cons": ["con 1", "con 2"]
|
|
}
|
|
},
|
|
"pros": ["overall pro 1", "overall pro 2"],
|
|
"cons": ["overall con 1", "overall con 2"],
|
|
"sources": [
|
|
{ "title": "source title", "url": "https://...", "snippet": "relevant excerpt" }
|
|
]
|
|
}
|
|
],
|
|
"dimensions": ["Dimension 1", "Dimension 2", ...],
|
|
"summary": "overall comparison summary highlighting key differences and trade-offs",
|
|
"recommendation": "clear recommendation with reasoning"
|
|
}
|
|
|
|
Important:
|
|
- Be factual and specific. Include real data where possible.
|
|
- Scores should be on a 1-10 scale where 10 is best.
|
|
- Provide at least 2-3 pros and cons per item.
|
|
- The top-level pros/cons for each item should be the most significant overall points.
|
|
- Dimension-level pros/cons should be specific to that dimension.
|
|
- Sources should be realistic URLs to relevant documentation or resources.
|
|
- The recommendation should consider different use cases when appropriate.`;
|
|
|
|
const MAX_RETRIES = 3;
|
|
const RETRY_DELAY_MS = 1000;
|
|
|
|
function sleep(ms: number) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function validateComparisonResult(data: unknown): data is ComparisonResult {
|
|
if (!data || typeof data !== "object") return false;
|
|
const result = data as Record<string, unknown>;
|
|
if (!Array.isArray(result.items)) return false;
|
|
if (!Array.isArray(result.dimensions)) return false;
|
|
if (typeof result.summary !== "string") return false;
|
|
if (typeof result.recommendation !== "string") return false;
|
|
|
|
for (const item of result.items as ItemResearch[]) {
|
|
if (typeof item.name !== "string") return false;
|
|
if (typeof item.description !== "string") return false;
|
|
if (typeof item.overallScore !== "number") return false;
|
|
if (!Array.isArray(item.pros)) return false;
|
|
if (!Array.isArray(item.cons)) return false;
|
|
if (typeof item.dimensions !== "object") return false;
|
|
|
|
for (const dim of Object.values(item.dimensions) as DimensionResult[]) {
|
|
if (typeof dim.score !== "number") return false;
|
|
if (typeof dim.summary !== "string") return false;
|
|
if (typeof dim.details !== "string") return false;
|
|
if (!Array.isArray(dim.pros)) return false;
|
|
if (!Array.isArray(dim.cons)) return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export async function generateComparison(
|
|
request: ComparisonRequest
|
|
): Promise<ComparisonResult> {
|
|
const userPrompt = `Compare the following items: ${request.items.join(", ")}
|
|
${request.query ? `Focus: ${request.query}` : ""}
|
|
${request.dimensions?.length ? `Specific dimensions to include: ${request.dimensions.join(", ")}` : ""}
|
|
|
|
Provide a comprehensive comparison with scores, pros/cons, and a recommendation.`;
|
|
|
|
let lastError: Error | null = null;
|
|
|
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
try {
|
|
const response = await getClient().chat.completions.create({
|
|
model: "gpt-4o-mini",
|
|
messages: [
|
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
{ role: "user", content: userPrompt },
|
|
],
|
|
response_format: { type: "json_object" },
|
|
temperature: 0.3,
|
|
});
|
|
|
|
const content = response.choices[0]?.message?.content;
|
|
if (!content) {
|
|
throw new Error("Empty response from OpenAI");
|
|
}
|
|
|
|
const parsed: unknown = JSON.parse(content);
|
|
|
|
if (!validateComparisonResult(parsed)) {
|
|
throw new Error("Invalid comparison result structure from OpenAI");
|
|
}
|
|
|
|
return parsed;
|
|
} catch (error) {
|
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
|
|
if (attempt < MAX_RETRIES) {
|
|
await sleep(RETRY_DELAY_MS * attempt);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
`Failed to generate comparison after ${MAX_RETRIES} attempts: ${lastError?.message}`
|
|
);
|
|
}
|