fix
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
||||
)
|
||||
|
||||
func TestBuildA1StyleEnhanceUserTemplate_mirrorsLegacyGenerator(t *testing.T) {
|
||||
t.Parallel()
|
||||
prompt := aiprompts.SectionTitleStart + "\n" +
|
||||
`Napiši novo ime izdelka po formuli: "tip izdelka sentence case", "znamka", "model". Ne uporabljaj vejic.` + "\n" +
|
||||
aiprompts.SectionTitleEnd + "\n\n" +
|
||||
aiprompts.SectionDescriptionStart + "\n" +
|
||||
`<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>` +
|
||||
`<p>{Napiši odstavek, ki je dolg 100 besed.}</p>` +
|
||||
`<b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij}</li></ul>` + "\n" +
|
||||
aiprompts.SectionDescriptionEnd + "\n\n" +
|
||||
aiprompts.SectionMetaStart + "\n" +
|
||||
`Najprej napiši Novo ime in potem nadaljuj do 140 znakov.` + "\n" +
|
||||
aiprompts.SectionMetaEnd
|
||||
|
||||
descTpl := aiprompts.EffectiveDescriptionTemplateAny(nil, prompt)
|
||||
if !categoryUsesA1StyleFormula(prompt, nil, descTpl) {
|
||||
t.Fatal("expected A1-style formula detection")
|
||||
}
|
||||
user := buildA1StyleEnhanceUserTemplate(prompt, nil, descTpl, false)
|
||||
for _, want := range []string{
|
||||
"GPT predloga:",
|
||||
"Sledi tej GPT predlogi",
|
||||
"Staro_ime_izdelka: {{name}}",
|
||||
"Star_opis_izdelka: {{description}}",
|
||||
"<name>{",
|
||||
"<metaDescription>{",
|
||||
"<H2>{",
|
||||
`"name"`,
|
||||
`"description"`,
|
||||
} {
|
||||
if !strings.Contains(user, want) {
|
||||
t.Fatalf("missing %q in:\n%s", want, user)
|
||||
}
|
||||
}
|
||||
|
||||
sys, renderedUser := resolveProductPromptTemplates(ProductInput{
|
||||
CategoryEnhancePrompt: prompt,
|
||||
Language: "sl",
|
||||
})
|
||||
if !strings.Contains(sys, "Si tekstopisec") {
|
||||
t.Fatalf("system must be A1 copywriter, got:\n%s", sys)
|
||||
}
|
||||
if !strings.Contains(renderedUser, "GPT predloga:") {
|
||||
t.Fatalf("user must be A1 GPT predloga, got:\n%s", renderedUser)
|
||||
}
|
||||
// Soft overlay framing must not win over GPT predloga.
|
||||
if strings.Contains(renderedUser, "Category guidance (applies to name") {
|
||||
t.Fatal("A1 path must not use soft category guidance overlay")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLegacyEnhanceHTML(t *testing.T) {
|
||||
t.Parallel()
|
||||
raw := `<name>ovitek PanzerGlass Honor 200 Smart črna</name>` +
|
||||
`<metaDescription>Ovitek PanzerGlass za Honor 200 Smart z elegantnim videzom.</metaDescription>` +
|
||||
`<h2>Zaščita z elegantnim videzom</h2><p>Dolg odstavek o ovitku.</p>` +
|
||||
`<ul><li>Barva: črna</li></ul>`
|
||||
name, meta, body, ok := ParseLegacyEnhanceHTML(raw)
|
||||
if !ok {
|
||||
t.Fatal("expected parse ok")
|
||||
}
|
||||
if name != "ovitek PanzerGlass Honor 200 Smart črna" {
|
||||
t.Fatalf("name=%q", name)
|
||||
}
|
||||
if !strings.Contains(meta, "PanzerGlass") {
|
||||
t.Fatalf("meta=%q", meta)
|
||||
}
|
||||
if strings.Contains(body, "<name>") || strings.Contains(body, "metaDescription") {
|
||||
t.Fatalf("body still has tags: %q", body)
|
||||
}
|
||||
if !strings.Contains(body, "<h2>") || !strings.Contains(body, "<ul>") {
|
||||
t.Fatalf("body missing HTML: %q", body)
|
||||
}
|
||||
|
||||
obj := map[string]any{
|
||||
"name": "",
|
||||
"description": raw,
|
||||
}
|
||||
applyLegacyEnhanceTagFallback(obj, "")
|
||||
if obj["name"] != name {
|
||||
t.Fatalf("fallback name=%v", obj["name"])
|
||||
}
|
||||
if strings.Contains(fmt.Sprint(obj["description"]), "<name>") {
|
||||
t.Fatalf("fallback desc still has name tag: %v", obj["description"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
||||
)
|
||||
|
||||
type a1SmokeCompleter struct {
|
||||
system, user string
|
||||
reply string
|
||||
}
|
||||
|
||||
func (c *a1SmokeCompleter) Complete(_ context.Context, system, user string) (Completion, error) {
|
||||
c.system, c.user = system, user
|
||||
return Completion{Text: c.reply, TotalTokens: 12}, nil
|
||||
}
|
||||
|
||||
func (c *a1SmokeCompleter) Enabled() bool { return true }
|
||||
|
||||
func TestEnhance_A1StyleFormulaLocalSmoke(t *testing.T) {
|
||||
t.Parallel()
|
||||
prompt := aiprompts.SectionTitleStart + "\n" +
|
||||
`Napiši novo ime izdelka po formuli: "tip izdelka sentence case", "znamka s pravilno kapitalizacijo", "model", barva v eni besedi. Ne uporabljaj vejic.` + "\n" +
|
||||
aiprompts.SectionTitleEnd + "\n\n" +
|
||||
aiprompts.SectionDescriptionStart + "\n" +
|
||||
`<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>` +
|
||||
`<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>` +
|
||||
`<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>` +
|
||||
`<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi tip izdelka lowercase}</p>` +
|
||||
`<b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>` + "\n" +
|
||||
aiprompts.SectionDescriptionEnd + "\n\n" +
|
||||
aiprompts.SectionMetaStart + "\n" +
|
||||
`Najprej napiši Novo ime izdelka in potem nadaljuj dokler nisi zapisal 140 znakov.` + "\n" +
|
||||
aiprompts.SectionMetaEnd
|
||||
|
||||
replyObj := map[string]any{
|
||||
"name": "ovitek PanzerGlass Honor 200 Smart črna",
|
||||
"description": "<h2>Zaščita z elegantnim videzom</h2><p>" + strings.Repeat("Besedilo o ovitku. ", 20) +
|
||||
"</p><h2>Trden oprijem</h2><p>" + strings.Repeat("Še več besedila. ", 20) +
|
||||
"</p><ul><li>Barva: črna</li><li>Model: Honor 200 Smart</li></ul>",
|
||||
"meta_title": "ovitek PanzerGlass Honor 200 Smart črna | zaščita",
|
||||
"meta_description": "ovitek PanzerGlass Honor 200 Smart črna z elegantnim videzom in trdnim oprijemom za vsakodnevno uporabo.",
|
||||
"attrs": map[string]any{"brand": "PanzerGlass"},
|
||||
}
|
||||
raw, _ := json.Marshal(replyObj)
|
||||
cap := &a1SmokeCompleter{reply: string(raw)}
|
||||
eng := &Engine{Completer: cap}
|
||||
|
||||
descTpl := aiprompts.EffectiveDescriptionTemplateAny(nil, prompt)
|
||||
name, desc, _, meta, err := eng.enhance(context.Background(), ProductInput{
|
||||
Name: "ovitek PanzerGlass 200 Smart",
|
||||
Description: "Zaščitni ovitek za telefon.",
|
||||
Language: "sl",
|
||||
CategoryEnhancePrompt: prompt,
|
||||
DescriptionTemplate: descTpl,
|
||||
CategoryUniqueID: "ovitki",
|
||||
}, "Ovitki in mape", map[string]any{"brand": "PanzerGlass", "product_model": "Honor 200 Smart"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(cap.system, "Si tekstopisec") {
|
||||
t.Fatalf("expected A1 system, got: %s", cap.system)
|
||||
}
|
||||
if !strings.Contains(cap.user, "GPT predloga:") || !strings.Contains(cap.user, "<name>{") {
|
||||
t.Fatalf("expected legacy GPT predloga user, got: %s", cap.user)
|
||||
}
|
||||
if !strings.Contains(cap.user, "Staro_ime_izdelka: ovitek PanzerGlass 200 Smart") {
|
||||
t.Fatalf("expected supplier name substituted like legacy STARO IME: %s", cap.user)
|
||||
}
|
||||
if !strings.Contains(cap.user, "<H2>{") && !strings.Contains(cap.user, "<h2>{") {
|
||||
t.Fatalf("expected HTML formula in predloga: %s", cap.user)
|
||||
}
|
||||
if name != "ovitek PanzerGlass Honor 200 Smart črna" {
|
||||
t.Fatalf("name=%q", name)
|
||||
}
|
||||
if !descriptionSatisfiesFormula(desc, descTpl) {
|
||||
t.Fatalf("description must satisfy formula: %s", desc)
|
||||
}
|
||||
st, _ := meta.(map[string]any)
|
||||
if st == nil || st["status"] != "ok" {
|
||||
t.Fatalf("meta=%#v", meta)
|
||||
}
|
||||
}
|
||||
@@ -77,24 +77,21 @@ func TestA1StyleDescriptionFormulaInResolvedPrompts(t *testing.T) {
|
||||
TitleTemplate: titleFormula,
|
||||
DescriptionTemplate: descFormula,
|
||||
})
|
||||
if !strings.Contains(sysTpl, "Description formula") {
|
||||
t.Fatalf("system missing formula override: %s", sysTpl)
|
||||
if !strings.Contains(sysTpl, "Si tekstopisec") {
|
||||
t.Fatalf("system missing A1 copywriter: %s", sysTpl)
|
||||
}
|
||||
if !strings.Contains(sysTpl, `When the user message includes a "Title formula"`) {
|
||||
t.Fatalf("system missing title formula override: %s", sysTpl)
|
||||
if !strings.Contains(userTpl, "GPT predloga:") || !strings.Contains(userTpl, "<H1>{Naziv izdelka}</H1>") {
|
||||
t.Fatalf("user missing A1 description predloga: %s", userTpl)
|
||||
}
|
||||
if !strings.Contains(userTpl, "Description formula") || !strings.Contains(userTpl, "- h1: Naziv izdelka") || !strings.Contains(userTpl, "- h2: Podnaslov prednosti") {
|
||||
t.Fatalf("user missing description formula: %s", userTpl)
|
||||
}
|
||||
if !strings.Contains(userTpl, "Title formula") || !strings.Contains(userTpl, "attr [brand]") {
|
||||
t.Fatalf("user missing title formula: %s", userTpl)
|
||||
if !strings.Contains(userTpl, "<name>{") || !strings.Contains(userTpl, "brand") {
|
||||
t.Fatalf("user missing A1 title predloga: %s", userTpl)
|
||||
}
|
||||
system, user := RenderProductEnhancePrompts(
|
||||
sysTpl, userTpl,
|
||||
"Štedilniki", "VOX EHT6020", "", "", "", "sl",
|
||||
map[string]any{"brand": "VOX", "product_model": "EHT6020"},
|
||||
)
|
||||
if !strings.Contains(user, "Description formula") || !strings.Contains(user, "- h1: Naziv izdelka") {
|
||||
if !strings.Contains(user, "GPT predloga:") || !strings.Contains(user, "Naziv izdelka") {
|
||||
t.Fatalf("rendered user missing formula: %s", user)
|
||||
}
|
||||
if !strings.Contains(user, "Slovenian") && !strings.Contains(system, "Slovenian") {
|
||||
|
||||
@@ -65,11 +65,12 @@ func TestAppendDescriptionFormulaSystemOverride(t *testing.T) {
|
||||
EnhanceUserTemplate: "Name: {{name}}",
|
||||
DescriptionTemplate: desc,
|
||||
})
|
||||
if !strings.Contains(user, "Description formula") {
|
||||
t.Fatalf("missing user formula: %s", user)
|
||||
// Structured description_template uses A1 GPT-predloga path (legacy generator.php).
|
||||
if !strings.Contains(user, "GPT predloga:") || !strings.Contains(user, "<H1>{Heading}</H1>") {
|
||||
t.Fatalf("missing A1 description predloga: %s", user)
|
||||
}
|
||||
if !strings.Contains(sys, "Description formula") || !strings.Contains(sys, "1-2 sentences") {
|
||||
t.Fatalf("system should keep company text and append formula override: %s", sys)
|
||||
if !strings.Contains(sys, "Si tekstopisec") {
|
||||
t.Fatalf("system should be A1 copywriter when formula set: %s", sys)
|
||||
}
|
||||
if AppendDescriptionFormulaSystemOverride("plain", nil) != "plain" {
|
||||
t.Fatal("empty formula must be no-op")
|
||||
@@ -90,16 +91,22 @@ func TestAppendTitleFormulaSystemOverride(t *testing.T) {
|
||||
EnhanceUserTemplate: "Name: {{name}}",
|
||||
TitleTemplate: title,
|
||||
})
|
||||
if !strings.Contains(user, "Title formula") || !strings.Contains(user, "attr [brand]") {
|
||||
t.Fatalf("missing user title formula: %s", user)
|
||||
if !strings.Contains(user, "GPT predloga:") || !strings.Contains(user, "<name>{") {
|
||||
t.Fatalf("missing A1 title predloga: %s", user)
|
||||
}
|
||||
if !strings.Contains(sys, `When the user message includes a "Title formula"`) || !strings.Contains(sys, "short retail title") {
|
||||
t.Fatalf("system should keep company text and append title override: %s", sys)
|
||||
if !strings.Contains(user, "brand") {
|
||||
t.Fatalf("missing brand in title formula: %s", user)
|
||||
}
|
||||
if !strings.Contains(sys, "Si tekstopisec") {
|
||||
t.Fatalf("system should be A1 copywriter when title formula set: %s", sys)
|
||||
}
|
||||
if AppendTitleFormulaSystemOverride("plain", nil) != "plain" {
|
||||
t.Fatal("empty title formula must be no-op")
|
||||
}
|
||||
dup := AppendTitleFormulaSystemOverride(sys, title)
|
||||
// Direct override helper stays idempotent for soft path.
|
||||
soft := "Retail product copywriter.\n- name: short retail title"
|
||||
with := AppendTitleFormulaSystemOverride(soft, title)
|
||||
dup := AppendTitleFormulaSystemOverride(with, title)
|
||||
if strings.Count(dup, `When the user message includes a "Title formula"`) != 1 {
|
||||
t.Fatalf("title override must be idempotent: %s", dup)
|
||||
}
|
||||
@@ -239,24 +246,24 @@ func TestAppendFormulaConstraints_injectedIntoResolve(t *testing.T) {
|
||||
TitleTemplate: title,
|
||||
DescriptionTemplate: desc,
|
||||
})
|
||||
if !strings.Contains(user, "Name: {{name}}") {
|
||||
t.Fatalf("lost base template: %s", user)
|
||||
if !strings.Contains(user, "GPT predloga:") {
|
||||
t.Fatalf("expected A1 GPT predloga: %s", user)
|
||||
}
|
||||
if !strings.Contains(user, "Title formula") || !strings.Contains(user, "attr [brand]") {
|
||||
t.Fatalf("missing title formula: %s", user)
|
||||
if !strings.Contains(user, "<name>{") || !strings.Contains(user, "brand") {
|
||||
t.Fatalf("missing title formula in predloga: %s", user)
|
||||
}
|
||||
if !strings.Contains(user, "Description formula") || !strings.Contains(user, "- p: Factual summary") {
|
||||
t.Fatalf("missing description formula: %s", user)
|
||||
if !strings.Contains(user, "<p>{Factual summary}</p>") {
|
||||
t.Fatalf("missing description formula HTML: %s", user)
|
||||
}
|
||||
if !strings.Contains(user, "SEO meta formula") || !strings.Contains(user, "meta_title (50-60 chars)") {
|
||||
t.Fatalf("missing SEO meta formula: %s", user)
|
||||
if !strings.Contains(user, "<metaDescription>{") || !strings.Contains(user, "120-155") {
|
||||
t.Fatalf("missing SEO meta in predloga: %s", user)
|
||||
}
|
||||
if !strings.Contains(sys, "SEO meta formula") {
|
||||
t.Fatalf("system missing meta override: %s", sys)
|
||||
if !strings.Contains(sys, "Si tekstopisec") {
|
||||
t.Fatalf("system missing A1 copywriter: %s", sys)
|
||||
}
|
||||
// Render substitutes {{language}} in formula blocks.
|
||||
_, rendered := RenderProductEnhancePrompts(
|
||||
"Write in {{language}}.", user,
|
||||
sys, user,
|
||||
"50", "Old Name", "", "", "", "sl",
|
||||
map[string]any{"brand": "Vox", "product_model": "EHT6020"},
|
||||
)
|
||||
|
||||
@@ -11,6 +11,19 @@ func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string)
|
||||
systemTpl = strings.TrimSpace(in.EnhanceSystemTemplate)
|
||||
userTpl = strings.TrimSpace(in.EnhanceUserTemplate)
|
||||
catPrompt := strings.TrimSpace(in.CategoryEnhancePrompt)
|
||||
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
|
||||
}
|
||||
|
||||
// Per-category prompt wins for the user message (company system keeps JSON schema / brand).
|
||||
if catPrompt != "" {
|
||||
userTpl = ensureCategoryEnhanceUserContext(catPrompt)
|
||||
@@ -24,9 +37,6 @@ func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string)
|
||||
userTpl = def.UserTemplate
|
||||
}
|
||||
}
|
||||
// Prefer stored description_template; else derive HTML sections from the
|
||||
// category prompt Description role (A1-style <H2>{…}</H2> overlays).
|
||||
descTpl := aiprompts.EffectiveDescriptionTemplateAny(in.DescriptionTemplate, catPrompt)
|
||||
// Category formulas are language-agnostic; inject once into the shared user skeleton.
|
||||
userTpl = AppendFormulaConstraints(userTpl, in.TitleTemplate, descTpl, in.OmitSEOMeta)
|
||||
// Free-form --- Title --- instructions when structured title_template is empty.
|
||||
|
||||
@@ -82,26 +82,18 @@ func TestResolveProductPromptTemplates_categoryWithTitleFormula(t *testing.T) {
|
||||
TitleTemplate: title,
|
||||
DescriptionTemplate: desc,
|
||||
})
|
||||
if !strings.Contains(user, "Emphasize energy class") {
|
||||
t.Fatalf("missing category prompt: %s", user)
|
||||
// Structured formulas trigger A1-style GPT predloga (legacy generator.php behavior).
|
||||
if !strings.Contains(sys, "Si tekstopisec") {
|
||||
t.Fatalf("expected A1 system when formulas set: %s", sys)
|
||||
}
|
||||
if !strings.Contains(user, "applies to name, description, and attrs") {
|
||||
t.Fatalf("missing category title framing: %s", user)
|
||||
if !strings.Contains(user, "GPT predloga:") {
|
||||
t.Fatalf("expected GPT predloga user: %s", user)
|
||||
}
|
||||
if !strings.Contains(user, "Title formula") || !strings.Contains(user, "attr [brand]") {
|
||||
t.Fatalf("missing title formula: %s", user)
|
||||
if !strings.Contains(user, "Emphasize energy class") && !strings.Contains(user, "<name>") {
|
||||
t.Fatalf("expected title/desc formula in predloga: %s", user)
|
||||
}
|
||||
if !strings.Contains(user, "Description formula") || !strings.Contains(user, "- p: Factual summary") {
|
||||
t.Fatalf("missing description formula: %s", user)
|
||||
}
|
||||
if !strings.Contains(sys, `When the user message includes a "Title formula"`) {
|
||||
t.Fatalf("missing title system override: %s", sys)
|
||||
}
|
||||
if !strings.Contains(sys, "Description formula") {
|
||||
t.Fatalf("missing description system override: %s", sys)
|
||||
}
|
||||
if !strings.Contains(sys, `apply it to "name", "description", and "attrs"`) {
|
||||
t.Fatalf("missing category system overlay: %s", sys)
|
||||
if !strings.Contains(user, "<p>{Factual summary}</p>") && !strings.Contains(user, "Factual summary") {
|
||||
t.Fatalf("missing description formula HTML: %s", user)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,20 +115,17 @@ func TestResolveProductPromptTemplates_derivesFormulaFromPromptHTML(t *testing.T
|
||||
// Empty structured template — production A1 path often only has prompt HTML.
|
||||
DescriptionTemplate: nil,
|
||||
})
|
||||
if !strings.Contains(user, "Description formula") {
|
||||
t.Fatalf("expected Description formula derived from prompt HTML:\n%s", user)
|
||||
if !strings.Contains(sys, "Si tekstopisec") {
|
||||
t.Fatalf("expected A1 copywriter system:\n%s", sys)
|
||||
}
|
||||
if !strings.Contains(user, "- h2:") || !strings.Contains(user, "- p:") || !strings.Contains(user, "- ul:") {
|
||||
t.Fatalf("expected h2/p/ul sections in formula:\n%s", user)
|
||||
if !strings.Contains(user, "GPT predloga:") || !strings.Contains(user, "<name>{") {
|
||||
t.Fatalf("expected legacy GPT predloga with <name>:\n%s", user)
|
||||
}
|
||||
if !strings.Contains(sys, "Description formula") {
|
||||
t.Fatalf("missing description system override:\n%s", sys)
|
||||
if !strings.Contains(user, "<H2>{") && !strings.Contains(user, "<h2>{") {
|
||||
t.Fatalf("expected HTML description formula in predloga:\n%s", user)
|
||||
}
|
||||
if !strings.Contains(sys, `"name" MUST be rewritten`) {
|
||||
t.Fatalf("missing title rewrite system override:\n%s", sys)
|
||||
}
|
||||
if !strings.Contains(user, "Title instructions (REQUIRED") {
|
||||
t.Fatalf("missing Title section constraint:\n%s", user)
|
||||
if strings.Contains(user, "Category guidance (applies to name") {
|
||||
t.Fatal("A1 path must not use soft category guidance overlay")
|
||||
}
|
||||
effective := aiprompts.EffectiveDescriptionTemplateAny(nil, prompt)
|
||||
if descriptionSatisfiesFormula("plain Slovenian prose without tags", effective) {
|
||||
|
||||
@@ -863,6 +863,22 @@ func (e *Engine) enhance(ctx context.Context, in ProductInput, category string,
|
||||
ReasoningEffort: "low",
|
||||
})
|
||||
elapsed := time.Since(started)
|
||||
if obj != nil {
|
||||
applyLegacyEnhanceTagFallback(obj, comp.Text)
|
||||
} else if err != nil && strings.TrimSpace(comp.Text) != "" {
|
||||
// Legacy A1 models sometimes reply with HTML <name>/<metaDescription> instead of JSON.
|
||||
if n, m, body, ok := ParseLegacyEnhanceHTML(comp.Text); ok && (n != "" || body != "") {
|
||||
obj = map[string]any{
|
||||
"name": n,
|
||||
"description": body,
|
||||
"meta_description": m,
|
||||
}
|
||||
if n != "" && m == "" {
|
||||
obj["meta_title"] = n
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// Network/provider failure vs parse failure after retry
|
||||
name := preferredProductTitle(in.GTIN, in.Name, in.PriorProcessedName)
|
||||
|
||||
Reference in New Issue
Block a user