579 lines
20 KiB
Go
579 lines
20 KiB
Go
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 → <hN>…</hN>, p → <p>…</p>, ul → <ul><li>…</li></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 == "<nil>" {
|
|
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
|
|
}
|
|
}
|