247 lines
9.3 KiB
Go
247 lines
9.3 KiB
Go
package processing
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
|
)
|
|
|
|
// Legacy A1 (generator.php process_product_description) used the category Prompt
|
|
// as the entire USER message after substituting OPIS IZDELKA / STARO IME IZDELKA,
|
|
// with a one-line Slovenian copywriter SYSTEM message, then parsed <name> and
|
|
// <metaDescription> from the HTML reply. v2 must recreate that GPT-predloga
|
|
// dominance while keeping JSON {"name","description","meta_*","attrs"}.
|
|
|
|
const a1StyleEnhanceSystemTemplate = `Si tekstopisec, ki piše opise izdelkov v {{language}}.
|
|
Rules:
|
|
- Reply with ONLY JSON (no markdown)
|
|
- Schema: {"name":"string","description":"string","meta_title":"string","meta_description":"string","attrs":{}}
|
|
- Obey the GPT predloga in the user message step by step (same behavior as legacy A1 category Prompt)
|
|
- "name" follows the <name> Title formula — never copy Staro_ime_izdelka unchanged when the formula asks for a new name
|
|
- "description" is ONE HTML string covering each GPT predloga body section in order (tags matching type: h1/h2/h3/h4, p, ul) — do NOT wrap the reply in <name> or <metaDescription> tags
|
|
- "meta_title" and "meta_description" are plain SEO text from the meta / <metaDescription> formula (never HTML body)
|
|
- attrs: only Allowed attribute keys when listed; fill from evidence only; omit unknowns
|
|
{{brand_voice}}`
|
|
|
|
var (
|
|
reLegacyNameCapture = regexp.MustCompile(`(?is)<\s*name\s*>\s*(.*?)\s*<\s*/\s*name\s*>`)
|
|
reLegacyMetaCapture = regexp.MustCompile(`(?is)<\s*metaDescription\s*>\s*(.*?)\s*<\s*/\s*metaDescription\s*>`)
|
|
reLegacyNameStrip = regexp.MustCompile(`(?is)<\s*name\s*>.*?<\s*/\s*name\s*>`)
|
|
reLegacyMetaStrip = regexp.MustCompile(`(?is)<\s*metaDescription\s*>.*?<\s*/\s*metaDescription\s*>`)
|
|
)
|
|
|
|
// categoryUsesA1StyleFormula is true when the category carries Title/Description/Meta
|
|
// formula content that must drive enhance like legacy generator.php (not soft overlay).
|
|
func categoryUsesA1StyleFormula(catPrompt string, titleTemplate, descriptionTemplate any) bool {
|
|
if FormatTitleFormulaConstraint(titleTemplate) != "" {
|
|
return true
|
|
}
|
|
if FormatDescriptionFormulaConstraint(descriptionTemplate) != "" {
|
|
return true
|
|
}
|
|
if FormatMetaFormulaConstraint(descriptionTemplate) != "" {
|
|
return true
|
|
}
|
|
if FormatTitleSectionConstraint(catPrompt) != "" {
|
|
return true
|
|
}
|
|
if aiprompts.CategoryTitlePromptRequiresRewrite(catPrompt) {
|
|
return true
|
|
}
|
|
descBody := aiprompts.DescriptionSectionInstructions(catPrompt)
|
|
if descBody != "" && aiprompts.LegacyHTMLFormulaPresent(descBody) {
|
|
return true
|
|
}
|
|
return strings.TrimSpace(aiprompts.MetaSectionInstructions(catPrompt)) != ""
|
|
}
|
|
|
|
// buildA1StyleEnhanceUserTemplate rebuilds the legacy GPT-predloga user message
|
|
// (Title → <name>, Meta → <metaDescription>, Description HTML blocks) plus product
|
|
// evidence placeholders, asking for JSON field mapping.
|
|
func buildA1StyleEnhanceUserTemplate(catPrompt string, titleTemplate, descriptionTemplate any, omitSEOMeta bool) string {
|
|
titleInstr := strings.TrimSpace(aiprompts.TitleSectionInstructions(catPrompt))
|
|
if titleInstr == "" || isBuiltInTitleRoleBoilerplate(titleInstr) {
|
|
titleInstr = ""
|
|
}
|
|
if titleInstr == "" {
|
|
if block := FormatTitleFormulaConstraint(titleTemplate); block != "" {
|
|
titleInstr = block
|
|
}
|
|
}
|
|
|
|
descBody := strings.TrimSpace(aiprompts.DescriptionSectionInstructions(catPrompt))
|
|
if descBody != "" && !aiprompts.LegacyHTMLFormulaPresent(descBody) {
|
|
// Free-form description instructions without HTML — keep as a p-block cue.
|
|
descBody = "<p>{" + descBody + "}</p>"
|
|
}
|
|
if descBody == "" {
|
|
descBody = FormatDescriptionFormulaAsLegacyHTML(descriptionTemplate)
|
|
}
|
|
|
|
metaInstr := strings.TrimSpace(aiprompts.MetaSectionInstructions(catPrompt))
|
|
if metaInstr == "" {
|
|
mt, md := parseMetaFormulaInstructions(descriptionTemplate)
|
|
switch {
|
|
case mt != "" && md != "":
|
|
metaInstr = "meta_title: " + mt + "\nmeta_description: " + md
|
|
case md != "":
|
|
metaInstr = md
|
|
case mt != "":
|
|
metaInstr = mt
|
|
}
|
|
}
|
|
|
|
var predloga strings.Builder
|
|
if titleInstr != "" {
|
|
fmt.Fprintf(&predloga, "<name>{%s}</name>\n", collapseLegacyInstr(titleInstr))
|
|
}
|
|
if !omitSEOMeta && metaInstr != "" {
|
|
fmt.Fprintf(&predloga, "<metaDescription>{%s}</metaDescription>\n", collapseLegacyInstr(metaInstr))
|
|
}
|
|
if descBody != "" {
|
|
predloga.WriteString(strings.TrimSpace(descBody))
|
|
predloga.WriteByte('\n')
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString("Ustvari nov opis izdelka v {{language}} z naslednjimi spremenljivkami:\n\n")
|
|
b.WriteString("Staro_ime_izdelka: {{name}}\n")
|
|
b.WriteString("Star_opis_izdelka: {{description}}\n")
|
|
b.WriteString("Kategorija: {{category}}\n")
|
|
b.WriteString("Attrs: {{attrs}}\n\n")
|
|
b.WriteString("Uporabi spodnjo GPT predlogo.\n")
|
|
b.WriteString("Sledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka.\n\n")
|
|
b.WriteString("GPT predloga:\n\n")
|
|
b.WriteString(strings.TrimSpace(predloga.String()))
|
|
b.WriteString("\n\n")
|
|
b.WriteString("Odgovori SAMO z enim JSON objektom:\n")
|
|
b.WriteString(`- "name" = rezultat <name> formule (novo ime; ne kopiraj Staro_ime_izdelka, če formula zahteva novo ime)` + "\n")
|
|
b.WriteString(`- "description" = HTML telo po GPT predlogi (vsi razdelki po vrsti), BREZ <name>/<metaDescription> tagov` + "\n")
|
|
if !omitSEOMeta {
|
|
b.WriteString(`- "meta_title" / "meta_description" = plain SEO po meta / <metaDescription> formuli` + "\n")
|
|
}
|
|
b.WriteString(`- "attrs" = atributi (samo Allowed attribute keys, če so navedeni)`)
|
|
|
|
// Free-form category overlay (no role markers) — keep as extra guidance like
|
|
// soft "Category guidance" did, without replacing the GPT predloga.
|
|
extra := strings.TrimSpace(catPrompt)
|
|
if extra != "" &&
|
|
!aiprompts.CategoryEnhanceHasRoleSections(extra) &&
|
|
!aiprompts.IsLegacyCombinedEnhancePrompt(extra) {
|
|
b.WriteString("\n\nDodatna kategorijska navodila:\n")
|
|
b.WriteString(extra)
|
|
}
|
|
return strings.TrimSpace(b.String())
|
|
}
|
|
|
|
func collapseLegacyInstr(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
|
for strings.Contains(s, "\n\n\n") {
|
|
s = strings.ReplaceAll(s, "\n\n\n", "\n\n")
|
|
}
|
|
return s
|
|
}
|
|
|
|
// FormatDescriptionFormulaAsLegacyHTML turns description_template sections back into
|
|
// the A1 HTML GPT-predloga shape (<H2>{…}</H2><p>{…}</p><ul>…).
|
|
func FormatDescriptionFormulaAsLegacyHTML(template any) string {
|
|
sections, ok := parseDescriptionFormulaSections(template)
|
|
if !ok || len(sections) == 0 {
|
|
return ""
|
|
}
|
|
var b strings.Builder
|
|
for _, s := range sections {
|
|
typ := strings.ToLower(strings.TrimSpace(s.Type))
|
|
instr := strings.TrimSpace(s.Instructions)
|
|
if instr == "" {
|
|
instr = typ
|
|
}
|
|
switch typ {
|
|
case "h1", "h2", "h3", "h4":
|
|
fmt.Fprintf(&b, "<%s>{%s}</%s>", strings.ToUpper(typ), instr, strings.ToUpper(typ))
|
|
case "ul", "ol", "li":
|
|
fmt.Fprintf(&b, "<b>{Tehnične specifikacije}</b><ul><li>{%s}</li></ul>", instr)
|
|
case "p", "":
|
|
fmt.Fprintf(&b, "<p>{%s}</p>", instr)
|
|
default:
|
|
fmt.Fprintf(&b, "<p>{%s}</p>", instr)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// ParseLegacyEnhanceHTML extracts name / metaDescription / body HTML from a legacy
|
|
// A1 model reply (generator.php preg_match on <name> / <metaDescription>).
|
|
func ParseLegacyEnhanceHTML(raw string) (name, metaDescription, bodyHTML string, ok bool) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return "", "", "", false
|
|
}
|
|
if m := reLegacyNameCapture.FindStringSubmatch(raw); len(m) == 2 {
|
|
name = strings.TrimSpace(m[1])
|
|
}
|
|
if m := reLegacyMetaCapture.FindStringSubmatch(raw); len(m) == 2 {
|
|
metaDescription = strings.TrimSpace(m[1])
|
|
}
|
|
body := reLegacyNameStrip.ReplaceAllString(raw, "")
|
|
body = reLegacyMetaStrip.ReplaceAllString(body, "")
|
|
bodyHTML = strings.TrimSpace(body)
|
|
ok = name != "" || metaDescription != "" || (bodyHTML != "" && bodyHTML != raw)
|
|
return name, metaDescription, bodyHTML, ok
|
|
}
|
|
|
|
// applyLegacyEnhanceTagFallback fills empty JSON fields from embedded legacy tags
|
|
// and strips <name>/<metaDescription> wrappers from description (feed.php behavior).
|
|
func applyLegacyEnhanceTagFallback(obj map[string]any, rawText string) {
|
|
if obj == nil {
|
|
return
|
|
}
|
|
name := strings.TrimSpace(fmt.Sprint(obj["name"]))
|
|
desc := strings.TrimSpace(fmt.Sprint(obj["description"]))
|
|
metaTitle := strings.TrimSpace(fmt.Sprint(obj["meta_title"]))
|
|
metaDesc := strings.TrimSpace(fmt.Sprint(obj["meta_description"]))
|
|
if metaDesc == "" {
|
|
metaDesc = strings.TrimSpace(fmt.Sprint(obj["metaDescription"]))
|
|
}
|
|
|
|
src := desc
|
|
if src == "" || src == "<nil>" {
|
|
src = rawText
|
|
}
|
|
ln, lm, lb, ok := ParseLegacyEnhanceHTML(src)
|
|
if !ok {
|
|
ln2, lm2, lb2, ok2 := ParseLegacyEnhanceHTML(rawText)
|
|
if ok2 {
|
|
ln, lm, lb, ok = ln2, lm2, lb2, true
|
|
}
|
|
}
|
|
if !ok {
|
|
// Still strip tags if description accidentally includes them.
|
|
if desc != "" && desc != "<nil>" {
|
|
cleaned := reLegacyNameStrip.ReplaceAllString(desc, "")
|
|
cleaned = reLegacyMetaStrip.ReplaceAllString(cleaned, "")
|
|
cleaned = strings.TrimSpace(cleaned)
|
|
if cleaned != "" && cleaned != desc {
|
|
obj["description"] = cleaned
|
|
}
|
|
}
|
|
return
|
|
}
|
|
if (name == "" || name == "<nil>") && ln != "" {
|
|
obj["name"] = ln
|
|
}
|
|
if (metaDesc == "" || metaDesc == "<nil>") && lm != "" {
|
|
obj["meta_description"] = lm
|
|
}
|
|
if (metaTitle == "" || metaTitle == "<nil>") && ln != "" {
|
|
// Legacy only had metaDescription; seed meta_title from new name when empty.
|
|
obj["meta_title"] = ln
|
|
}
|
|
if lb != "" {
|
|
obj["description"] = lb
|
|
}
|
|
}
|