package processing import ( "strings" "github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts" "github.com/descrybe/descrybe-v2/apps/api/internal/company" ) func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string) { systemTpl = strings.TrimSpace(in.EnhanceSystemTemplate) userTpl = strings.TrimSpace(in.EnhanceUserTemplate) catPrompt := strings.TrimSpace(in.CategoryEnhancePrompt) // Per-category prompt wins for the user message (company system keeps JSON schema / brand). if catPrompt != "" { userTpl = ensureCategoryEnhanceUserContext(catPrompt) } def, ok := aiprompts.DefaultFor(aiprompts.KeyProductEnhance) if ok { if systemTpl == "" { systemTpl = def.SystemTemplate } if userTpl == "" { userTpl = def.UserTemplate } } // Prefer stored description_template; else derive HTML sections from the // category prompt Description role (A1-style

{…}

overlays). descTpl := aiprompts.EffectiveDescriptionTemplateAny(in.DescriptionTemplate, catPrompt) // Category formulas are language-agnostic; inject once into the shared user skeleton. userTpl = AppendFormulaConstraints(userTpl, in.TitleTemplate, descTpl, in.OmitSEOMeta) // Category attribute allowlist + title-formula keys guide JSON "attrs" extraction. allowed := enhanceAllowedAttrKeys(in, in.CategoryUniqueID) userTpl = AppendAttributeConstraints(userTpl, allowed, in.TitleTemplate) // Company/built-in system prompts often say "1-2 sentences"; when a category // description formula exists, override that so process matches A1 category defs. systemTpl = AppendDescriptionFormulaSystemOverride(systemTpl, descTpl) // Same for title_template vs "short retail title" — formulas win for name structure. systemTpl = AppendTitleFormulaSystemOverride(systemTpl, in.TitleTemplate) // Free-form Title section (prompt UI) that asks for a rewrite — not title_template. systemTpl = AppendTitlePromptRewriteOverride(systemTpl, catPrompt) if !in.OmitSEOMeta { systemTpl = AppendMetaFormulaSystemOverride(systemTpl, descTpl) } // categories.prompt applies to name, description, and attrs (not description-only). systemTpl = AppendCategoryEnhanceSystemOverlay(systemTpl, catPrompt) return systemTpl, userTpl } // thinEnhanceUserInstruction is prepended when Desc is empty or ≈ Name so the model // builds copy from Attrs+Category instead of echoing the title. const thinEnhanceUserInstruction = "Build the description from Attrs and Category only; never copy Name." // templateHasVar reports whether tpl contains {{name}} (via aiprompts.ExtractVariables). func templateHasVar(tpl, name string) bool { for _, v := range aiprompts.ExtractVariables(tpl) { if v == name { return true } } return false } // categoryEnhanceUserOverlayPrefix frames free-form categories.prompt text so the // model applies it to name, description, and attrs (not description alone). Canonical // CategoryEnhanceUserTemplate already covers all roles — skip double-framing there. const categoryEnhanceUserOverlayPrefix = "Category guidance (applies to name, description, and attrs; obey Title/Description formulas and Allowed attribute keys when present):\n" func categoryEnhancePromptAlreadyCoversTitle(userTpl string) bool { lower := strings.ToLower(userTpl) if strings.Contains(lower, "applies to name, description, and attrs") { return true } if strings.Contains(lower, "applies to name and description") { return true } // Repaired / built-in overlay: explicit name + Title formula bullets / role sections. return strings.Contains(lower, `"name"`) && (strings.Contains(lower, "title formula") || strings.Contains(lower, "--- title ---")) } // ensureCategoryEnhanceUserContext frames category overlay for title+description, // then appends standard product context placeholders when a category override omits // {{attrs}} (common in DB category prompts). Does not rewrite category copy that // already includes attrs (aside from optional title framing). func ensureCategoryEnhanceUserContext(userTpl string) string { userTpl = strings.TrimSpace(userTpl) if userTpl == "" { return userTpl } if !categoryEnhancePromptAlreadyCoversTitle(userTpl) { userTpl = categoryEnhanceUserOverlayPrefix + userTpl } if templateHasVar(userTpl, "attrs") { return userTpl } var b strings.Builder b.WriteString(userTpl) b.WriteString("\n\n") first := true appendLine := func(line string) { if !first { b.WriteByte('\n') } first = false b.WriteString(line) } if !templateHasVar(userTpl, "category") { appendLine("Category: {{category}}") } if !templateHasVar(userTpl, "name") { appendLine("Name: {{name}}") } if !templateHasVar(userTpl, "description") { appendLine("Description: {{description}}") } appendLine("Attrs: {{attrs}}") return b.String() } func isThinEnhanceDescription(desc, name string) bool { desc = strings.TrimSpace(desc) if desc == "" || desc == "" { return true } return descriptionEchoesTitle(desc, name) } // RenderProductEnhancePrompts fills company/built-in templates with product variables. func RenderProductEnhancePrompts(systemTpl, userTpl, category, name, description, gtin, brandPrompt, language string, attrs map[string]any) (system, user string) { attrsJSON := "" compact := CompactAttrs(attrs, MaxAttrKeys) if len(compact) > 0 { attrsJSON = sanitizeJSON(compact) } vars := aiprompts.Vars{ "name": SanitizeText(truncateRunes(name, 200)), "description": SanitizeText(truncateRunes(description, MaxProductDescRunes)), "category": SanitizeText(category), "attrs": attrsJSON, "gtin": SanitizeText(gtin), "brand_voice": CompactBrandPrompt(brandPrompt), "language": company.LanguageLabel(language), } system = strings.TrimSpace(aiprompts.Render(systemTpl, vars)) user = strings.TrimSpace(aiprompts.Render(userTpl, vars)) if user == "" { // Safety net if a custom user template renders empty. user = ProductEnhanceUser(category, name, description, attrs) } if isThinEnhanceDescription(description, name) { user = thinEnhanceUserInstruction + "\n\n" + user } return system, user }