package processing import ( "encoding/json" "fmt" "strconv" "strings" ) // AppendFormulaConstraints appends language-agnostic title/description 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. func AppendFormulaConstraints(userTpl string, titleTemplate, descriptionTemplate any) string { userTpl = strings.TrimSpace(userTpl) titleBlock := FormatTitleFormulaConstraint(titleTemplate) descBlock := FormatDescriptionFormulaConstraint(descriptionTemplate) if titleBlock == "" && descBlock == "" { return userTpl } var b strings.Builder if userTpl != "" { b.WriteString(userTpl) b.WriteString("\n\n") } if titleBlock != "" { b.WriteString(titleBlock) if descBlock != "" { b.WriteString("\n\n") } } if descBlock != "" { b.WriteString(descBlock) } return b.String() } // FormatTitleFormulaConstraint turns categories.title_template into enhance-prompt // constraints (element order: literal text + attribute variable keys). func FormatTitleFormulaConstraint(template any) string { 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}}.") 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 } 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 } // 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 } 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 } }