package processing import ( "encoding/json" "fmt" "sort" "strconv" "strings" "github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts" "github.com/descrybe/descrybe-v2/apps/api/internal/company" ) // AppendFormulaConstraints appends language-agnostic title/description/meta formula // guidance to the enhance user template (before {{var}} render). Empty templates // are no-ops. Shared skeleton stays in company/built-in prompts; formulas only // constrain structure for the active language via {{language}} elsewhere. // Meta instructions come from description_template.metaTitle / metaDescription // (legacy A1 / cats.json) and are distinct from HTML description sections. // Attribute allowlist / formula-key guidance is AppendAttributeConstraints. // When omitSEOMeta is true (A1 cohort), meta formula blocks are skipped. func AppendFormulaConstraints(userTpl string, titleTemplate, descriptionTemplate any, omitSEOMeta bool) string { userTpl = strings.TrimSpace(userTpl) blocks := []string{ FormatTitleFormulaConstraint(titleTemplate), FormatDescriptionFormulaConstraint(descriptionTemplate), } if !omitSEOMeta { blocks = append(blocks, FormatMetaFormulaConstraint(descriptionTemplate)) } var joined strings.Builder for _, block := range blocks { if block == "" { continue } if joined.Len() > 0 { joined.WriteString("\n\n") } joined.WriteString(block) } if joined.Len() == 0 { return userTpl } if userTpl == "" { return joined.String() } return userTpl + "\n\n" + joined.String() } // FormatTitleSectionConstraint turns free-form --- Title --- section instructions // into a required enhance constraint when no structured title_template elements exist. func FormatTitleSectionConstraint(categoryPrompt string) string { body := strings.TrimSpace(aiprompts.TitleSectionInstructions(categoryPrompt)) if body == "" || isBuiltInTitleRoleBoilerplate(body) { return "" } var b strings.Builder b.WriteString("Title instructions (REQUIRED — overrides any shorter \"short retail title\" rule):\n") b.WriteString(body) b.WriteString("\nBuild JSON \"name\" exactly per these instructions in {{language}}.") b.WriteString(" Never copy the supplier Name unchanged when instructions ask for a new name or formula.") return b.String() } func isBuiltInTitleRoleBoilerplate(body string) bool { lower := strings.ToLower(strings.TrimSpace(body)) return strings.HasPrefix(lower, "role: title") } // AppendTitleSectionConstraint appends FormatTitleSectionConstraint when the // category Title role has text and structured title_template is empty/unusable. func AppendTitleSectionConstraint(userTpl, categoryPrompt string, titleTemplate any) string { if FormatTitleFormulaConstraint(titleTemplate) != "" { return strings.TrimSpace(userTpl) } block := FormatTitleSectionConstraint(categoryPrompt) if block == "" { return strings.TrimSpace(userTpl) } userTpl = strings.TrimSpace(userTpl) if strings.Contains(userTpl, "Title instructions (REQUIRED") { return userTpl } if userTpl == "" { return block } return userTpl + "\n\n" + block } // MaxAttrAllowlistPromptKeys caps Allowed attribute keys listed in enhance prompts. const MaxAttrAllowlistPromptKeys = 40 // AppendAttributeConstraints appends category_attributes allowlist + title-formula // variable keys so the model can emit JSON "attrs" validated via AttrsForEnhance. // No-op when both allowlist and formula keys are empty. func AppendAttributeConstraints(userTpl string, allowed map[string]struct{}, titleTemplate any) string { userTpl = strings.TrimSpace(userTpl) block := FormatAttributeAllowlistConstraint(allowed, titleTemplate) if block == "" { return userTpl } if userTpl == "" { return block } if strings.Contains(userTpl, `Allowed attribute keys (JSON "attrs" object`) { return userTpl } return userTpl + "\n\n" + block } // FormatAttributeAllowlistConstraint lists category allowlist keys and title-formula // attr slots for the Attributes enhance role. When allowed is nil, only formula // keys are listed (unit-test / sanitize-only paths). func FormatAttributeAllowlistConstraint(allowed map[string]struct{}, titleTemplate any) string { keys := preferredAttrKeyList(allowed, MaxAttrAllowlistPromptKeys) formulaKeys := titleFormulaAttrKeys(titleTemplate) if len(keys) == 0 && len(formulaKeys) == 0 { return "" } var b strings.Builder b.WriteString("Allowed attribute keys (JSON \"attrs\" object — remap feed labels onto these; omit unknowns):\n") if len(keys) > 0 { b.WriteString("- category: ") b.WriteString(strings.Join(keys, ", ")) b.WriteByte('\n') } else { b.WriteString("- category: (core characteristics only — brand, product_model, dims, …)\n") } if len(formulaKeys) > 0 { b.WriteString("- title formula slots: ") b.WriteString(strings.Join(formulaKeys, ", ")) b.WriteByte('\n') } b.WriteString("Fill missing keys only from Name/Description/Category/Attrs evidence; never invent specs or zero dimensions.") return strings.TrimSpace(b.String()) } func preferredAttrKeyList(allowed map[string]struct{}, maxKeys int) []string { if len(allowed) == 0 || maxKeys <= 0 { return nil } prefer := preferredAllowedKeys(allowed) seen := map[string]struct{}{} out := make([]string, 0, len(prefer)) for _, k := range prefer { k = strings.TrimSpace(k) if k == "" { continue } if _, ok := seen[k]; ok { continue } seen[k] = struct{}{} out = append(out, k) } sort.Strings(out) if len(out) > maxKeys { out = out[:maxKeys] } return out } func titleFormulaAttrKeys(template any) []string { elements, _, ok := parseTitleFormula(template) if !ok || len(elements) == 0 { return nil } seen := map[string]struct{}{} var out []string for _, el := range elements { if el.Type != "variable" { continue } k := strings.TrimSpace(el.Value) if k == "" { continue } canon := canonicalizeAttrKey(k) if canon == "" { canon = strings.ToLower(k) } if _, ok := seen[canon]; ok { continue } seen[canon] = struct{}{} out = append(out, canon) } sort.Strings(out) return out } // FormatTitleFormulaConstraint turns categories.title_template into enhance-prompt // constraints (element order: literal text + attribute variable keys). // // Brand-only stubs ({elements:[brand]}, the shape an older A1 migration wrote onto // every category) are not a formula: instructing "name = brand" yields a // brand-only title that the title picker then rejects in favour of the supplier // name — an LLM call spent to reproduce the feed. Treat them as absent so the // category prompt / built-in retail rules drive the name instead. func FormatTitleFormulaConstraint(template any) string { if aiprompts.TitleTemplateIsBrandOnly(template) { return "" } elements, separator, ok := parseTitleFormula(template) if !ok || len(elements) == 0 { return "" } sep := separator if sep == "" { sep = " " } var b strings.Builder b.WriteString("Title formula (order matters; join with ") b.WriteString(strconv.Quote(sep)) b.WriteString("). Build name from Attrs using this structure:\n") for i, el := range elements { switch el.Type { case "text": fmt.Fprintf(&b, "%d. text %s\n", i+1, strconv.Quote(el.Value)) case "variable": key := strings.TrimSpace(el.Value) if key == "" { continue } fmt.Fprintf(&b, "%d. attr [%s]\n", i+1, key) default: continue } } b.WriteString("Prefer Attrs values for [attr] slots; keep literal text as written; write name in {{language}}; never emit brand-only when product_type or product_model slots exist.") return strings.TrimSpace(b.String()) } // FormatMetaFormulaConstraint turns description_template.metaTitle / metaDescription // (cats.json / A1 SEO instructions) into enhance-prompt constraints distinct from // HTML description sections. func FormatMetaFormulaConstraint(template any) string { metaTitle, metaDesc := parseMetaFormulaInstructions(template) if metaTitle == "" && metaDesc == "" { return "" } var b strings.Builder b.WriteString("SEO meta formula (REQUIRED — distinct from description HTML; plain text only):\n") b.WriteString("Emit meta_title and meta_description as separate JSON fields in {{language}}.\n") if metaTitle != "" { fmt.Fprintf(&b, "- meta_title (50-60 chars): %s\n", metaTitle) } else { b.WriteString("- meta_title: 50-60 chars, product name + main benefit or use case\n") } if metaDesc != "" { fmt.Fprintf(&b, "- meta_description (120-155 chars): %s\n", metaDesc) } else { b.WriteString("- meta_description: 120-155 chars, factual SEO snippet; never HTML\n") } return strings.TrimSpace(b.String()) } // FormatDescriptionFormulaConstraint turns categories.description_template sections // into bullet instructions for the enhance user prompt. func FormatDescriptionFormulaConstraint(template any) string { sections, ok := parseDescriptionFormulaSections(template) if !ok || len(sections) == 0 { return "" } var b strings.Builder b.WriteString("Description formula (REQUIRED — overrides any shorter \"1-2 sentences\" rule):\n") b.WriteString("Emit description as ONE HTML string covering each section in order in {{language}}.\n") b.WriteString("Use tags matching section type: h1/h2/h3/h4 → , p →

, ul → .\n") for _, s := range sections { typ := strings.TrimSpace(s.Type) instr := strings.TrimSpace(s.Instructions) if typ == "" && instr == "" { continue } if typ == "" { typ = "section" } if instr == "" { fmt.Fprintf(&b, "- %s\n", typ) continue } fmt.Fprintf(&b, "- %s: %s\n", typ, instr) } return strings.TrimSpace(b.String()) } // descriptionFormulaSystemOverride is appended to the enhance system template when a // category description_template is present so company/built-in "1-2 sentences" rules // cannot override the per-category formula (A1 category definition pages). const descriptionFormulaSystemOverride = `When the user message includes a "Description formula", obey those sections over any shorter "1-2 sentences" / "1-3 paragraphs" guidance: emit one HTML string using tags matching each section type (h1/h2/h3/h4, p, ul), in order, in {{language}}. Never ignore the Description formula.` // AppendDescriptionFormulaSystemOverride strengthens the system prompt when a // description formula is active. No-op when template is empty/unparseable. func AppendDescriptionFormulaSystemOverride(systemTpl string, descriptionTemplate any) string { if FormatDescriptionFormulaConstraint(descriptionTemplate) == "" { return strings.TrimSpace(systemTpl) } systemTpl = strings.TrimSpace(systemTpl) if systemTpl == "" { return descriptionFormulaSystemOverride } if strings.Contains(systemTpl, "Description formula") { return systemTpl } return systemTpl + "\n" + descriptionFormulaSystemOverride } // titleFormulaSystemOverride is appended to the enhance system template when a // category title_template is present so company/built-in "short retail title" // rules cannot ignore the Title formula (mirrors description formula override). const titleFormulaSystemOverride = `When the user message includes a "Title formula", obey that structure for "name" over any shorter "short retail title" guidance: build name from Attrs in the given element order, keep literal text as written, write name in {{language}}. Never ignore the Title formula. Never emit brand-only when the formula includes product_type or product_model.` // AppendTitleFormulaSystemOverride strengthens the system prompt when a title // formula is active. No-op when template is empty/unparseable. func AppendTitleFormulaSystemOverride(systemTpl string, titleTemplate any) string { if FormatTitleFormulaConstraint(titleTemplate) == "" { return strings.TrimSpace(systemTpl) } systemTpl = strings.TrimSpace(systemTpl) if systemTpl == "" { return titleFormulaSystemOverride } if strings.Contains(systemTpl, `When the user message includes a "Title formula"`) { return systemTpl } return systemTpl + "\n" + titleFormulaSystemOverride } // titlePromptRewriteOverride covers free-form Title section instructions // (categories.prompt --- Title ---) that are not structured title_template. const titlePromptRewriteOverride = `When the category Title section asks for a new product name or naming formula, "name" MUST be rewritten — never copy the supplier Name unchanged when type/brand/model/color (or other Title cues) are available. Obey the Title section over any "keep short retail title" guidance.` // AppendTitlePromptRewriteOverride strengthens the system prompt when the // category Title role requires a rewrite. No-op when Title does not ask for one. func AppendTitlePromptRewriteOverride(systemTpl, categoryPrompt string) string { if !aiprompts.CategoryTitlePromptRequiresRewrite(categoryPrompt) { return strings.TrimSpace(systemTpl) } systemTpl = strings.TrimSpace(systemTpl) if systemTpl == "" { return titlePromptRewriteOverride } if strings.Contains(systemTpl, `"name" MUST be rewritten`) { return systemTpl } return systemTpl + "\n" + titlePromptRewriteOverride } // categoryEnhanceSystemOverlay is appended when categories.prompt (enhance overlay) // is active so free-form category copy drives title, description, AND attributes — // not description alone — while Title/Description formulas keep structural precedence. const categoryEnhanceSystemOverlay = `When the user message includes category guidance, apply it to "name", "description", and "attrs" (tone/focus for the title and body, plus attribute extraction onto Allowed attribute keys). Structural Title/Description formula blocks and Allowed attribute keys in the user message still take precedence when present.` // AppendCategoryEnhanceSystemOverlay strengthens the system prompt when a // per-category enhance overlay (categories.prompt) is set. No-op when empty. func AppendCategoryEnhanceSystemOverlay(systemTpl, categoryPrompt string) string { if strings.TrimSpace(categoryPrompt) == "" { return strings.TrimSpace(systemTpl) } systemTpl = strings.TrimSpace(systemTpl) if systemTpl == "" { return categoryEnhanceSystemOverlay } marker := `apply it to "name", "description", and "attrs"` if strings.Contains(systemTpl, marker) { return systemTpl } // Upgrade older name+description-only overlay without stacking duplicates. legacy := `apply it to BOTH "name" and "description"` if strings.Contains(systemTpl, legacy) { return strings.Replace(systemTpl, legacy, marker, 1) } return systemTpl + "\n" + categoryEnhanceSystemOverlay } // metaFormulaSystemOverride is appended when description_template carries metaTitle // / metaDescription instructions so enhance emits SEO fields separately from HTML. const metaFormulaSystemOverride = `When the user message includes an "SEO meta formula", emit meta_title and meta_description as plain SEO text (not HTML) distinct from description. Obey the character guidance in the formula.` // AppendMetaFormulaSystemOverride strengthens the system prompt when meta formula // instructions are present on description_template. No-op when absent. func AppendMetaFormulaSystemOverride(systemTpl string, descriptionTemplate any) string { if FormatMetaFormulaConstraint(descriptionTemplate) == "" { return strings.TrimSpace(systemTpl) } systemTpl = strings.TrimSpace(systemTpl) if systemTpl == "" { return metaFormulaSystemOverride } if strings.Contains(systemTpl, "SEO meta formula") { return systemTpl } return systemTpl + "\n" + metaFormulaSystemOverride } // descriptionSatisfiesFormula reports whether desc includes the HTML tags required // by categories.description_template sections. No formula → always true. func descriptionSatisfiesFormula(desc string, template any) bool { sections, ok := parseDescriptionFormulaSections(template) if !ok || len(sections) == 0 { return true } desc = strings.TrimSpace(desc) if desc == "" || desc == "" { return false } types := make([]string, 0, len(sections)) for _, s := range sections { types = append(types, s.Type) } return !company.DescriptionMissingFormulaHTMLTags(desc, types) } type titleFormulaElement struct { Type string `json:"type"` Value string `json:"value"` } type descriptionFormulaSection struct { Type string `json:"type"` Instructions string `json:"instructions"` } func parseTitleFormula(template any) (elements []titleFormulaElement, separator string, ok bool) { if template == nil { return nil, "", false } obj, err := asObjectMap(template) if err != nil || obj == nil { return nil, "", false } sep, _ := obj["separator"].(string) rawEls, exists := obj["elements"] if !exists || rawEls == nil { return nil, "", false } b, err := json.Marshal(rawEls) if err != nil { return nil, "", false } var parsed []titleFormulaElement if err := json.Unmarshal(b, &parsed); err != nil { return nil, "", false } out := make([]titleFormulaElement, 0, len(parsed)) for _, el := range parsed { t := strings.ToLower(strings.TrimSpace(el.Type)) v := strings.TrimSpace(el.Value) if t != "text" && t != "variable" { continue } if v == "" { continue } out = append(out, titleFormulaElement{Type: t, Value: v}) } if len(out) == 0 { return nil, "", false } return out, sep, true } func parseDescriptionFormulaSections(template any) ([]descriptionFormulaSection, bool) { if template == nil { return nil, false } obj, err := asObjectMap(template) if err != nil || obj == nil { return nil, false } raw, exists := obj["sections"] if !exists || raw == nil { return nil, false } b, err := json.Marshal(raw) if err != nil { return nil, false } var parsed []descriptionFormulaSection if err := json.Unmarshal(b, &parsed); err != nil { return nil, false } out := make([]descriptionFormulaSection, 0, len(parsed)) for _, s := range parsed { t := strings.ToLower(strings.TrimSpace(s.Type)) instr := strings.TrimSpace(s.Instructions) if t == "" && instr == "" { continue } out = append(out, descriptionFormulaSection{Type: t, Instructions: instr}) } return out, len(out) > 0 } // parseMetaFormulaInstructions reads metaTitle / metaDescription instruction // strings from description_template (camelCase as stored by the category UI). func parseMetaFormulaInstructions(template any) (metaTitle, metaDescription string) { if template == nil { return "", "" } obj, err := asObjectMap(template) if err != nil || obj == nil { return "", "" } metaTitle = strings.TrimSpace(stringFromAny(obj["metaTitle"])) if metaTitle == "" { metaTitle = strings.TrimSpace(stringFromAny(obj["meta_title"])) } metaDescription = strings.TrimSpace(stringFromAny(obj["metaDescription"])) if metaDescription == "" { metaDescription = strings.TrimSpace(stringFromAny(obj["meta_description"])) } return metaTitle, metaDescription } // categoryFormulasFor resolves title/description templates for a category key // (unique_id or name). Explicit ProductInput fields win over the job cache map. func categoryFormulasFor(in ProductInput, category string) (title, description any) { if in.TitleTemplate != nil || in.DescriptionTemplate != nil { return in.TitleTemplate, in.DescriptionTemplate } key := strings.ToLower(strings.TrimSpace(category)) if key == "" || len(in.CategoryFormulasByKey) == 0 { return nil, nil } f, ok := in.CategoryFormulasByKey[key] if !ok { return nil, nil } return f.TitleTemplate, f.DescriptionTemplate } // effectiveDescriptionTemplateFor resolves the description formula RunSteps should // judge a category's copy against: the category's own template, else one derived // from its prompt, else the platform default. Mirrors what e.enhance renders, so // the final repair pass cannot demand a different formula than the one the model // was given. func effectiveDescriptionTemplateFor(in ProductInput, out StepResult) any { category := strings.TrimSpace(out.Category) _, descTpl := categoryFormulasFor(in, category) catPrompt := strings.TrimSpace(in.CategoryEnhancePrompt) if catPrompt == "" { catPrompt = categoryEnhancePromptFor(in.CategoryPromptsByLang, category, in.Language, in.Language) } if effective := aiprompts.EffectiveDescriptionTemplateAny(descTpl, catPrompt); effective != nil { return effective } if catPrompt != "" || descTpl != nil { return descTpl } probe := ProductInput{ CategoryUniqueID: category, CategoryDisplayName: categoryDisplayLabel(out), } return withDefaultCategoryFormula(probe).DescriptionTemplate } func asObjectMap(v any) (map[string]any, error) { switch t := v.(type) { case map[string]any: return t, nil case nil: return nil, nil default: b, err := json.Marshal(t) if err != nil { return nil, err } if len(b) == 0 || string(b) == "null" { return nil, nil } var obj map[string]any if err := json.Unmarshal(b, &obj); err != nil { return nil, err } return obj, nil } }