Shopify AI Product Optimizer Ai Suggestions source
Imported TypeScript file from worker/services/ai-suggestions.ts.
Source file
worker/services/ai-suggestions.ts
TypeScript254 lines
import OpenAI from "openai";import type { SEOAnalysis, SEOIssue } from "./seo-analyzer";import { DEFAULT_PRODUCT_OPTIMIZATION_MODEL } from "../../app/lib/ai-models";interface ProductData {id: string;title?: string;handle?: string;descriptionHtml?: string;seo?: {title?: string;description?: string;};productType?: string;vendor?: string;tags?: string[];}interface ShopSettings {aiProvider?: string | null;aiModel?: string | null;brandVoice?: string | null;targetAudience?: string | null;primaryLanguage?: string | null;}export interface Suggestion {field: string;currentValue: string;suggestedValue: string;rationale: string;riskLevel: "low" | "medium" | "high";}const openai = new OpenAI({apiKey: process.env.OPENAI_API_KEY,});export async function generateSuggestions(product: ProductData,analysis: SEOAnalysis,shopSettings: ShopSettings): Promise<Suggestion[]> {const suggestions: Suggestion[] = [];// Group issues by fieldconst issuesByField = analysis.issues.reduce((acc, issue) => {if (!acc[issue.field]) {acc[issue.field] = [];}acc[issue.field].push(issue);return acc;}, {} as Record<string, SEOIssue[]>);// Generate suggestions for each field with issuesfor (const [field, issues] of Object.entries(issuesByField)) {try {const suggestion = await generateFieldSuggestion(product,field,issues,shopSettings);if (suggestion) {suggestions.push(suggestion);}} catch (error) {console.error(`Error generating suggestion for ${field}:`, error);}}return suggestions;}async function generateFieldSuggestion(product: ProductData,field: string,issues: SEOIssue[],shopSettings: ShopSettings): Promise<Suggestion | null> {const currentValue = getCurrentValue(product, field);const issueMessages = issues.map((i) => i.message).join("\n- ");const systemPrompt = buildSystemPrompt(shopSettings);const userPrompt = buildUserPrompt(product, field, currentValue, issueMessages);const response = await openai.chat.completions.create({model: shopSettings.aiModel || DEFAULT_PRODUCT_OPTIMIZATION_MODEL,messages: [{ role: "system", content: systemPrompt },{ role: "user", content: userPrompt },],response_format: { type: "json_object" },temperature: 0.7,max_tokens: 1000,});const content = response.choices[0]?.message?.content;if (!content) return null;try {const parsed = JSON.parse(content);return {field: mapFieldName(field),currentValue: currentValue || "",suggestedValue: parsed.suggestion,rationale: parsed.rationale,riskLevel: determineRiskLevel(field),};} catch {console.error("Failed to parse AI response:", content);return null;}}function buildSystemPrompt(settings: ShopSettings): string {let prompt = `You are an SEO expert for e-commerce. Your task is to generate optimized content for product listings.Guidelines:- Write clear, compelling copy that appeals to both search engines and humans- Include relevant keywords naturally- Follow SEO best practices for length and formatting- Be specific and descriptive`;if (settings.brandVoice) {prompt += `\n\nBrand voice: ${settings.brandVoice}`;}if (settings.targetAudience) {prompt += `\n\nTarget audience: ${settings.targetAudience}`;}if (settings.primaryLanguage && settings.primaryLanguage !== "en") {prompt += `\n\nWrite in: ${getLanguageName(settings.primaryLanguage)}`;}prompt += `\n\nRespond with a JSON object containing:{"suggestion": "your optimized content","rationale": "brief explanation of improvements made"}`;return prompt;}function buildUserPrompt(product: ProductData,field: string,currentValue: string | undefined,issues: string): string {let prompt = `Optimize the ${getFieldLabel(field)} for this product:\n\n`;prompt += `Product: ${product.title}\n`;if (product.productType) prompt += `Type: ${product.productType}\n`;if (product.vendor) prompt += `Brand: ${product.vendor}\n`;if (product.tags?.length) prompt += `Tags: ${product.tags.join(", ")}\n`;prompt += `\nCurrent ${getFieldLabel(field)}:\n${currentValue || "(empty)"}\n`;prompt += `\nIssues to address:\n- ${issues}\n`;// Add field-specific instructionsswitch (field) {case "seoTitle":prompt += `\nRequirements: 30-60 characters, include primary keyword, be compelling.`;break;case "seoDescription":prompt += `\nRequirements: 120-160 characters, include call-to-action, summarize key benefits.`;break;case "title":prompt += `\nRequirements: 20-70 characters, clear and descriptive, include product type.`;break;case "description":prompt += `\nRequirements: Use HTML formatting (<p>, <ul>, <strong>), at least 100 chars, highlight features and benefits.`;break;}return prompt;}function getCurrentValue(product: ProductData, field: string): string | undefined {switch (field) {case "title":return product.title;case "seoTitle":return product.seo?.title;case "seoDescription":return product.seo?.description;case "description":return product.descriptionHtml;case "handle":return product.handle;case "tags":return JSON.stringify(product.tags);default:return undefined;}}function mapFieldName(field: string): string {const mapping: Record<string, string> = {seoTitle: "seoTitle",seoDescription: "seoDescription",title: "title",description: "description",handle: "handle",tags: "tags",};return mapping[field] || field;}function determineRiskLevel(field: string): "low" | "medium" | "high" {switch (field) {case "seoTitle":case "seoDescription":return "low"; // These don't affect displaycase "tags":return "low";case "title":return "medium"; // Affects display but not URLscase "description":return "medium";case "handle":return "high"; // Affects URLs, requires redirectsdefault:return "medium";}}function getFieldLabel(field: string): string {const labels: Record<string, string> = {seoTitle: "SEO title",seoDescription: "meta description",title: "product title",description: "product description",handle: "URL handle",tags: "product tags",};return labels[field] || field;}function getLanguageName(code: string): string {const names: Record<string, string> = {en: "English",es: "Spanish",fr: "French",de: "German",pt: "Portuguese",it: "Italian",nl: "Dutch",ja: "Japanese",};return names[code] || "English";}