Code
    Shopify AI Product OptimizerUpdated Jul 13, 2026TS

    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
    1. import OpenAI from "openai";
    2. import type { SEOAnalysis, SEOIssue } from "./seo-analyzer";
    3. import { DEFAULT_PRODUCT_OPTIMIZATION_MODEL } from "../../app/lib/ai-models";
    4. interface ProductData {
    5. id: string;
    6. title?: string;
    7. handle?: string;
    8. descriptionHtml?: string;
    9. seo?: {
    10. title?: string;
    11. description?: string;
    12. };
    13. productType?: string;
    14. vendor?: string;
    15. tags?: string[];
    16. }
    17. interface ShopSettings {
    18. aiProvider?: string | null;
    19. aiModel?: string | null;
    20. brandVoice?: string | null;
    21. targetAudience?: string | null;
    22. primaryLanguage?: string | null;
    23. }
    24. export interface Suggestion {
    25. field: string;
    26. currentValue: string;
    27. suggestedValue: string;
    28. rationale: string;
    29. riskLevel: "low" | "medium" | "high";
    30. }
    31. const openai = new OpenAI({
    32. apiKey: process.env.OPENAI_API_KEY,
    33. });
    34. export async function generateSuggestions(
    35. product: ProductData,
    36. analysis: SEOAnalysis,
    37. shopSettings: ShopSettings
    38. ): Promise<Suggestion[]> {
    39. const suggestions: Suggestion[] = [];
    40. // Group issues by field
    41. const issuesByField = analysis.issues.reduce((acc, issue) => {
    42. if (!acc[issue.field]) {
    43. acc[issue.field] = [];
    44. }
    45. acc[issue.field].push(issue);
    46. return acc;
    47. }, {} as Record<string, SEOIssue[]>);
    48. // Generate suggestions for each field with issues
    49. for (const [field, issues] of Object.entries(issuesByField)) {
    50. try {
    51. const suggestion = await generateFieldSuggestion(
    52. product,
    53. field,
    54. issues,
    55. shopSettings
    56. );
    57. if (suggestion) {
    58. suggestions.push(suggestion);
    59. }
    60. } catch (error) {
    61. console.error(`Error generating suggestion for ${field}:`, error);
    62. }
    63. }
    64. return suggestions;
    65. }
    66. async function generateFieldSuggestion(
    67. product: ProductData,
    68. field: string,
    69. issues: SEOIssue[],
    70. shopSettings: ShopSettings
    71. ): Promise<Suggestion | null> {
    72. const currentValue = getCurrentValue(product, field);
    73. const issueMessages = issues.map((i) => i.message).join("\n- ");
    74. const systemPrompt = buildSystemPrompt(shopSettings);
    75. const userPrompt = buildUserPrompt(product, field, currentValue, issueMessages);
    76. const response = await openai.chat.completions.create({
    77. model: shopSettings.aiModel || DEFAULT_PRODUCT_OPTIMIZATION_MODEL,
    78. messages: [
    79. { role: "system", content: systemPrompt },
    80. { role: "user", content: userPrompt },
    81. ],
    82. response_format: { type: "json_object" },
    83. temperature: 0.7,
    84. max_tokens: 1000,
    85. });
    86. const content = response.choices[0]?.message?.content;
    87. if (!content) return null;
    88. try {
    89. const parsed = JSON.parse(content);
    90. return {
    91. field: mapFieldName(field),
    92. currentValue: currentValue || "",
    93. suggestedValue: parsed.suggestion,
    94. rationale: parsed.rationale,
    95. riskLevel: determineRiskLevel(field),
    96. };
    97. } catch {
    98. console.error("Failed to parse AI response:", content);
    99. return null;
    100. }
    101. }
    102. function buildSystemPrompt(settings: ShopSettings): string {
    103. let prompt = `You are an SEO expert for e-commerce. Your task is to generate optimized content for product listings.
    104. Guidelines:
    105. - Write clear, compelling copy that appeals to both search engines and humans
    106. - Include relevant keywords naturally
    107. - Follow SEO best practices for length and formatting
    108. - Be specific and descriptive`;
    109. if (settings.brandVoice) {
    110. prompt += `\n\nBrand voice: ${settings.brandVoice}`;
    111. }
    112. if (settings.targetAudience) {
    113. prompt += `\n\nTarget audience: ${settings.targetAudience}`;
    114. }
    115. if (settings.primaryLanguage && settings.primaryLanguage !== "en") {
    116. prompt += `\n\nWrite in: ${getLanguageName(settings.primaryLanguage)}`;
    117. }
    118. prompt += `\n\nRespond with a JSON object containing:
    119. {
    120. "suggestion": "your optimized content",
    121. "rationale": "brief explanation of improvements made"
    122. }`;
    123. return prompt;
    124. }
    125. function buildUserPrompt(
    126. product: ProductData,
    127. field: string,
    128. currentValue: string | undefined,
    129. issues: string
    130. ): string {
    131. let prompt = `Optimize the ${getFieldLabel(field)} for this product:\n\n`;
    132. prompt += `Product: ${product.title}\n`;
    133. if (product.productType) prompt += `Type: ${product.productType}\n`;
    134. if (product.vendor) prompt += `Brand: ${product.vendor}\n`;
    135. if (product.tags?.length) prompt += `Tags: ${product.tags.join(", ")}\n`;
    136. prompt += `\nCurrent ${getFieldLabel(field)}:\n${currentValue || "(empty)"}\n`;
    137. prompt += `\nIssues to address:\n- ${issues}\n`;
    138. // Add field-specific instructions
    139. switch (field) {
    140. case "seoTitle":
    141. prompt += `\nRequirements: 30-60 characters, include primary keyword, be compelling.`;
    142. break;
    143. case "seoDescription":
    144. prompt += `\nRequirements: 120-160 characters, include call-to-action, summarize key benefits.`;
    145. break;
    146. case "title":
    147. prompt += `\nRequirements: 20-70 characters, clear and descriptive, include product type.`;
    148. break;
    149. case "description":
    150. prompt += `\nRequirements: Use HTML formatting (<p>, <ul>, <strong>), at least 100 chars, highlight features and benefits.`;
    151. break;
    152. }
    153. return prompt;
    154. }
    155. function getCurrentValue(product: ProductData, field: string): string | undefined {
    156. switch (field) {
    157. case "title":
    158. return product.title;
    159. case "seoTitle":
    160. return product.seo?.title;
    161. case "seoDescription":
    162. return product.seo?.description;
    163. case "description":
    164. return product.descriptionHtml;
    165. case "handle":
    166. return product.handle;
    167. case "tags":
    168. return JSON.stringify(product.tags);
    169. default:
    170. return undefined;
    171. }
    172. }
    173. function mapFieldName(field: string): string {
    174. const mapping: Record<string, string> = {
    175. seoTitle: "seoTitle",
    176. seoDescription: "seoDescription",
    177. title: "title",
    178. description: "description",
    179. handle: "handle",
    180. tags: "tags",
    181. };
    182. return mapping[field] || field;
    183. }
    184. function determineRiskLevel(field: string): "low" | "medium" | "high" {
    185. switch (field) {
    186. case "seoTitle":
    187. case "seoDescription":
    188. return "low"; // These don't affect display
    189. case "tags":
    190. return "low";
    191. case "title":
    192. return "medium"; // Affects display but not URLs
    193. case "description":
    194. return "medium";
    195. case "handle":
    196. return "high"; // Affects URLs, requires redirects
    197. default:
    198. return "medium";
    199. }
    200. }
    201. function getFieldLabel(field: string): string {
    202. const labels: Record<string, string> = {
    203. seoTitle: "SEO title",
    204. seoDescription: "meta description",
    205. title: "product title",
    206. description: "product description",
    207. handle: "URL handle",
    208. tags: "product tags",
    209. };
    210. return labels[field] || field;
    211. }
    212. function getLanguageName(code: string): string {
    213. const names: Record<string, string> = {
    214. en: "English",
    215. es: "Spanish",
    216. fr: "French",
    217. de: "German",
    218. pt: "Portuguese",
    219. it: "Italian",
    220. nl: "Dutch",
    221. ja: "Japanese",
    222. };
    223. return names[code] || "English";
    224. }