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; 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 { 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}` ); }