2026-08-16 16:57:36 +02:00
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
2026-08-16 19:21:49 +02:00
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" )
2026-08-16 16:57:36 +02:00
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 ())
}
2026-08-16 19:21:49 +02:00
// 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
}
2026-08-16 16:57:36 +02:00
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
}
}