2026-08-09 22:47:43 +02:00
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 )
2026-08-16 23:42:00 +02:00
catPrompt := strings . TrimSpace ( in . CategoryEnhancePrompt )
2026-08-23 18:58:03 +02:00
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
}
2026-08-09 22:47:43 +02:00
// Per-category prompt wins for the user message (company system keeps JSON schema / brand).
2026-08-16 23:42:00 +02:00
if catPrompt != "" {
userTpl = ensureCategoryEnhanceUserContext ( catPrompt )
2026-08-09 22:47:43 +02:00
}
def , ok := aiprompts . DefaultFor ( aiprompts . KeyProductEnhance )
2026-08-16 16:57:36 +02:00
if ok {
if systemTpl == "" {
systemTpl = def . SystemTemplate
}
if userTpl == "" {
userTpl = def . UserTemplate
}
2026-08-09 22:47:43 +02:00
}
2026-08-16 16:57:36 +02:00
// Category formulas are language-agnostic; inject once into the shared user skeleton.
2026-08-23 13:39:56 +02:00
userTpl = AppendFormulaConstraints ( userTpl , in . TitleTemplate , descTpl , in . OmitSEOMeta )
2026-08-23 16:34:50 +02:00
// Free-form --- Title --- instructions when structured title_template is empty.
userTpl = AppendTitleSectionConstraint ( userTpl , catPrompt , in . TitleTemplate )
2026-08-16 23:42:00 +02:00
// Category attribute allowlist + title-formula keys guide JSON "attrs" extraction.
allowed := enhanceAllowedAttrKeys ( in , in . CategoryUniqueID )
userTpl = AppendAttributeConstraints ( userTpl , allowed , in . TitleTemplate )
2026-08-16 19:21:49 +02:00
// Company/built-in system prompts often say "1-2 sentences"; when a category
// description formula exists, override that so process matches A1 category defs.
2026-08-23 13:39:56 +02:00
systemTpl = AppendDescriptionFormulaSystemOverride ( systemTpl , descTpl )
2026-08-16 23:42:00 +02:00
// Same for title_template vs "short retail title" — formulas win for name structure.
systemTpl = AppendTitleFormulaSystemOverride ( systemTpl , in . TitleTemplate )
2026-08-23 13:39:56 +02:00
// Free-form Title section (prompt UI) that asks for a rewrite — not title_template.
systemTpl = AppendTitlePromptRewriteOverride ( systemTpl , catPrompt )
2026-08-17 01:30:28 +02:00
if ! in . OmitSEOMeta {
2026-08-23 13:39:56 +02:00
systemTpl = AppendMetaFormulaSystemOverride ( systemTpl , descTpl )
2026-08-17 01:30:28 +02:00
}
2026-08-16 23:42:00 +02:00
// categories.prompt applies to name, description, and attrs (not description-only).
systemTpl = AppendCategoryEnhanceSystemOverlay ( systemTpl , catPrompt )
2026-08-16 16:57:36 +02:00
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
}
2026-08-09 22:47:43 +02:00
}
2026-08-16 16:57:36 +02:00
return false
}
2026-08-16 23:42:00 +02:00
// 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).
2026-08-16 16:57:36 +02:00
func ensureCategoryEnhanceUserContext ( userTpl string ) string {
userTpl = strings . TrimSpace ( userTpl )
2026-08-16 23:42:00 +02:00
if userTpl == "" {
return userTpl
}
if ! categoryEnhancePromptAlreadyCoversTitle ( userTpl ) {
userTpl = categoryEnhanceUserOverlayPrefix + userTpl
}
if templateHasVar ( userTpl , "attrs" ) {
2026-08-16 16:57:36 +02:00
return userTpl
2026-08-09 22:47:43 +02:00
}
2026-08-16 16:57:36 +02:00
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" ) {
2026-08-16 23:42:00 +02:00
appendLine ( "Description: {{description}}" )
2026-08-16 16:57:36 +02:00
}
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 )
2026-08-09 22:47:43 +02:00
}
// 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 )
}
2026-08-16 16:57:36 +02:00
if isThinEnhanceDescription ( description , name ) {
user = thinEnhanceUserInstruction + "\n\n" + user
}
2026-08-09 22:47:43 +02:00
return system , user
}