fix
This commit is contained in:
@@ -3,38 +3,146 @@ package processing
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
||||
)
|
||||
|
||||
// AppendFormulaConstraints appends language-agnostic title/description formula
|
||||
// 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.
|
||||
func AppendFormulaConstraints(userTpl string, titleTemplate, descriptionTemplate any) string {
|
||||
userTpl = strings.TrimSpace(userTpl)
|
||||
titleBlock := FormatTitleFormulaConstraint(titleTemplate)
|
||||
descBlock := FormatDescriptionFormulaConstraint(descriptionTemplate)
|
||||
if titleBlock == "" && descBlock == "" {
|
||||
blocks := []string{
|
||||
FormatTitleFormulaConstraint(titleTemplate),
|
||||
FormatDescriptionFormulaConstraint(descriptionTemplate),
|
||||
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()
|
||||
}
|
||||
|
||||
// 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
|
||||
if userTpl != "" {
|
||||
b.WriteString(userTpl)
|
||||
b.WriteString("\n\n")
|
||||
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 titleBlock != "" {
|
||||
b.WriteString(titleBlock)
|
||||
if descBlock != "" {
|
||||
b.WriteString("\n\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)
|
||||
}
|
||||
if descBlock != "" {
|
||||
b.WriteString(descBlock)
|
||||
sort.Strings(out)
|
||||
if len(out) > maxKeys {
|
||||
out = out[:maxKeys]
|
||||
}
|
||||
return b.String()
|
||||
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
|
||||
@@ -70,6 +178,30 @@ func FormatTitleFormulaConstraint(template any) string {
|
||||
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 {
|
||||
@@ -120,6 +252,74 @@ func AppendDescriptionFormulaSystemOverride(systemTpl string, descriptionTemplat
|
||||
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.`
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -219,6 +419,27 @@ func parseDescriptionFormulaSections(template any) ([]descriptionFormulaSection,
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user