167 lines
6.6 KiB
Go
167 lines
6.6 KiB
Go
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)
|
|
descTpl := aiprompts.EffectiveDescriptionTemplateAny(in.DescriptionTemplate, catPrompt)
|
|
|
|
// Legacy A1 path: category Prompt WAS the user message (GPT predloga with
|
|
// <name>/<metaDescription>/HTML). When formulas are set, rebuild that shape
|
|
// instead of burying instructions under the generic retail copywriter overlay.
|
|
if categoryUsesA1StyleFormula(catPrompt, in.TitleTemplate, descTpl) {
|
|
userTpl = buildA1StyleEnhanceUserTemplate(catPrompt, in.TitleTemplate, descTpl, in.OmitSEOMeta)
|
|
systemTpl = a1StyleEnhanceSystemTemplate
|
|
allowed := enhanceAllowedAttrKeys(in, in.CategoryUniqueID)
|
|
userTpl = AppendAttributeConstraints(userTpl, allowed, in.TitleTemplate)
|
|
return systemTpl, userTpl
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
// Category formulas are language-agnostic; inject once into the shared user skeleton.
|
|
userTpl = AppendFormulaConstraints(userTpl, in.TitleTemplate, descTpl, in.OmitSEOMeta)
|
|
// Free-form --- Title --- instructions when structured title_template is empty.
|
|
userTpl = AppendTitleSectionConstraint(userTpl, catPrompt, in.TitleTemplate)
|
|
// 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 == "<nil>" {
|
|
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
|
|
}
|