Files
descrybe/apps/api/internal/processing/formula_prompt.go
T
2026-08-16 19:21:49 +02:00

240 lines
7.0 KiB
Go

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
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")
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())
}
// 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
}
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
}
}