Files
descrybe/apps/api/internal/processing/prompt_render.go
T
2026-08-16 16:57:36 +02:00

112 lines
3.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)
// Per-category prompt wins for the user message (company system keeps JSON schema / brand).
if cat := strings.TrimSpace(in.CategoryEnhancePrompt); cat != "" {
userTpl = ensureCategoryEnhanceUserContext(cat)
}
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, in.DescriptionTemplate)
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
}
// ensureCategoryEnhanceUserContext 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.
func ensureCategoryEnhanceUserContext(userTpl string) string {
userTpl = strings.TrimSpace(userTpl)
if userTpl == "" || 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("Desc: {{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
}