fix
This commit is contained in:
@@ -11,6 +11,12 @@ var (
|
|||||||
reLegacyBoldUL = regexp.MustCompile(`(?is)<\s*b\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*b\s*>\s*<\s*ul\s*>\s*<\s*li\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*li\s*>\s*<\s*/\s*ul\s*>`)
|
reLegacyBoldUL = regexp.MustCompile(`(?is)<\s*b\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*b\s*>\s*<\s*ul\s*>\s*<\s*li\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*li\s*>\s*<\s*/\s*ul\s*>`)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LegacyHTMLFormulaPresent reports whether s contains A1-style HTML formula blocks
|
||||||
|
// (<H2>{…}</H2>, <p>{…}</p>, …) used in category Description GPT predloga.
|
||||||
|
func LegacyHTMLFormulaPresent(s string) bool {
|
||||||
|
return reLegacyHTMLBlock.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
// DescriptionFormulaSection is one ordered block in categories.description_template.
|
// DescriptionFormulaSection is one ordered block in categories.description_template.
|
||||||
type DescriptionFormulaSection struct {
|
type DescriptionFormulaSection struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -182,6 +188,9 @@ func EffectiveDescriptionFormula(stored any, categoryPrompt string) DescriptionF
|
|||||||
// Unsectioned prompt may still be a legacy HTML blob.
|
// Unsectioned prompt may still be a legacy HTML blob.
|
||||||
descBody = strings.TrimSpace(categoryPrompt)
|
descBody = strings.TrimSpace(categoryPrompt)
|
||||||
}
|
}
|
||||||
|
if isBuiltInDescriptionRoleBoilerplate(descBody) {
|
||||||
|
descBody = ""
|
||||||
|
}
|
||||||
var derived DescriptionFormula
|
var derived DescriptionFormula
|
||||||
if descBody != "" && reLegacyHTMLBlock.MatchString(descBody) {
|
if descBody != "" && reLegacyHTMLBlock.MatchString(descBody) {
|
||||||
derived = DeriveDescriptionFormulaFromLegacyParts(LegacyEnhanceParts{
|
derived = DeriveDescriptionFormulaFromLegacyParts(LegacyEnhanceParts{
|
||||||
@@ -194,6 +203,9 @@ func EffectiveDescriptionFormula(stored any, categoryPrompt string) DescriptionF
|
|||||||
}
|
}
|
||||||
|
|
||||||
metaBody := ExtractEnhanceSectionBody(categoryPrompt, SectionMetaStart, SectionMetaEnd)
|
metaBody := ExtractEnhanceSectionBody(categoryPrompt, SectionMetaStart, SectionMetaEnd)
|
||||||
|
if isBuiltInMetaRoleBoilerplate(metaBody) {
|
||||||
|
metaBody = ""
|
||||||
|
}
|
||||||
if strings.TrimSpace(out.MetaTitle) == "" && strings.TrimSpace(out.MetaDescription) == "" && strings.TrimSpace(metaBody) != "" {
|
if strings.TrimSpace(out.MetaTitle) == "" && strings.TrimSpace(out.MetaDescription) == "" && strings.TrimSpace(metaBody) != "" {
|
||||||
out.MetaDescription = strings.TrimSpace(metaBody)
|
out.MetaDescription = strings.TrimSpace(metaBody)
|
||||||
out.MetaTitle = DefaultLegacyMetaTitleRule
|
out.MetaTitle = DefaultLegacyMetaTitleRule
|
||||||
@@ -208,6 +220,16 @@ func EffectiveDescriptionFormula(stored any, categoryPrompt string) DescriptionF
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isBuiltInMetaRoleBoilerplate(body string) bool {
|
||||||
|
lower := strings.ToLower(strings.TrimSpace(body))
|
||||||
|
return strings.HasPrefix(lower, "role: meta")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBuiltInDescriptionRoleBoilerplate(body string) bool {
|
||||||
|
lower := strings.ToLower(strings.TrimSpace(body))
|
||||||
|
return strings.HasPrefix(lower, "role: description")
|
||||||
|
}
|
||||||
|
|
||||||
func promptHasRoleMarkers(prompt string) bool {
|
func promptHasRoleMarkers(prompt string) bool {
|
||||||
lower := strings.ToLower(prompt)
|
lower := strings.ToLower(prompt)
|
||||||
return strings.Contains(lower, strings.ToLower(SectionTitleStart)) ||
|
return strings.Contains(lower, strings.ToLower(SectionTitleStart)) ||
|
||||||
@@ -220,6 +242,26 @@ func TitleSectionInstructions(categoryPrompt string) string {
|
|||||||
return ExtractEnhanceSectionBody(categoryPrompt, SectionTitleStart, SectionTitleEnd)
|
return ExtractEnhanceSectionBody(categoryPrompt, SectionTitleStart, SectionTitleEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DescriptionSectionInstructions returns the --- Description --- body, or empty
|
||||||
|
// when it is only the built-in Role: description boilerplate.
|
||||||
|
func DescriptionSectionInstructions(categoryPrompt string) string {
|
||||||
|
body := ExtractEnhanceSectionBody(categoryPrompt, SectionDescriptionStart, SectionDescriptionEnd)
|
||||||
|
if isBuiltInDescriptionRoleBoilerplate(body) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetaSectionInstructions returns the --- Meta --- body, or empty when it is only
|
||||||
|
// the built-in Role: meta boilerplate.
|
||||||
|
func MetaSectionInstructions(categoryPrompt string) string {
|
||||||
|
body := ExtractEnhanceSectionBody(categoryPrompt, SectionMetaStart, SectionMetaEnd)
|
||||||
|
if isBuiltInMetaRoleBoilerplate(body) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
// EffectiveDescriptionTemplateAny is EffectiveDescriptionFormula as a JSON object
|
// EffectiveDescriptionTemplateAny is EffectiveDescriptionFormula as a JSON object
|
||||||
// for processing.ProductInput.DescriptionTemplate (nil when empty).
|
// for processing.ProductInput.DescriptionTemplate (nil when empty).
|
||||||
func EffectiveDescriptionTemplateAny(stored any, categoryPrompt string) any {
|
func EffectiveDescriptionTemplateAny(stored any, categoryPrompt string) any {
|
||||||
|
|||||||
@@ -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,
|
TitleTemplate: titleFormula,
|
||||||
DescriptionTemplate: descFormula,
|
DescriptionTemplate: descFormula,
|
||||||
})
|
})
|
||||||
if !strings.Contains(sysTpl, "Description formula") {
|
if !strings.Contains(sysTpl, "Si tekstopisec") {
|
||||||
t.Fatalf("system missing formula override: %s", sysTpl)
|
t.Fatalf("system missing A1 copywriter: %s", sysTpl)
|
||||||
}
|
}
|
||||||
if !strings.Contains(sysTpl, `When the user message includes a "Title formula"`) {
|
if !strings.Contains(userTpl, "GPT predloga:") || !strings.Contains(userTpl, "<H1>{Naziv izdelka}</H1>") {
|
||||||
t.Fatalf("system missing title formula override: %s", sysTpl)
|
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") {
|
if !strings.Contains(userTpl, "<name>{") || !strings.Contains(userTpl, "brand") {
|
||||||
t.Fatalf("user missing description formula: %s", userTpl)
|
t.Fatalf("user missing A1 title predloga: %s", userTpl)
|
||||||
}
|
|
||||||
if !strings.Contains(userTpl, "Title formula") || !strings.Contains(userTpl, "attr [brand]") {
|
|
||||||
t.Fatalf("user missing title formula: %s", userTpl)
|
|
||||||
}
|
}
|
||||||
system, user := RenderProductEnhancePrompts(
|
system, user := RenderProductEnhancePrompts(
|
||||||
sysTpl, userTpl,
|
sysTpl, userTpl,
|
||||||
"Štedilniki", "VOX EHT6020", "", "", "", "sl",
|
"Štedilniki", "VOX EHT6020", "", "", "", "sl",
|
||||||
map[string]any{"brand": "VOX", "product_model": "EHT6020"},
|
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)
|
t.Fatalf("rendered user missing formula: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(user, "Slovenian") && !strings.Contains(system, "Slovenian") {
|
if !strings.Contains(user, "Slovenian") && !strings.Contains(system, "Slovenian") {
|
||||||
|
|||||||
@@ -65,11 +65,12 @@ func TestAppendDescriptionFormulaSystemOverride(t *testing.T) {
|
|||||||
EnhanceUserTemplate: "Name: {{name}}",
|
EnhanceUserTemplate: "Name: {{name}}",
|
||||||
DescriptionTemplate: desc,
|
DescriptionTemplate: desc,
|
||||||
})
|
})
|
||||||
if !strings.Contains(user, "Description formula") {
|
// Structured description_template uses A1 GPT-predloga path (legacy generator.php).
|
||||||
t.Fatalf("missing user formula: %s", user)
|
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") {
|
if !strings.Contains(sys, "Si tekstopisec") {
|
||||||
t.Fatalf("system should keep company text and append formula override: %s", sys)
|
t.Fatalf("system should be A1 copywriter when formula set: %s", sys)
|
||||||
}
|
}
|
||||||
if AppendDescriptionFormulaSystemOverride("plain", nil) != "plain" {
|
if AppendDescriptionFormulaSystemOverride("plain", nil) != "plain" {
|
||||||
t.Fatal("empty formula must be no-op")
|
t.Fatal("empty formula must be no-op")
|
||||||
@@ -90,16 +91,22 @@ func TestAppendTitleFormulaSystemOverride(t *testing.T) {
|
|||||||
EnhanceUserTemplate: "Name: {{name}}",
|
EnhanceUserTemplate: "Name: {{name}}",
|
||||||
TitleTemplate: title,
|
TitleTemplate: title,
|
||||||
})
|
})
|
||||||
if !strings.Contains(user, "Title formula") || !strings.Contains(user, "attr [brand]") {
|
if !strings.Contains(user, "GPT predloga:") || !strings.Contains(user, "<name>{") {
|
||||||
t.Fatalf("missing user title formula: %s", user)
|
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") {
|
if !strings.Contains(user, "brand") {
|
||||||
t.Fatalf("system should keep company text and append title override: %s", sys)
|
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" {
|
if AppendTitleFormulaSystemOverride("plain", nil) != "plain" {
|
||||||
t.Fatal("empty title formula must be no-op")
|
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 {
|
if strings.Count(dup, `When the user message includes a "Title formula"`) != 1 {
|
||||||
t.Fatalf("title override must be idempotent: %s", dup)
|
t.Fatalf("title override must be idempotent: %s", dup)
|
||||||
}
|
}
|
||||||
@@ -239,24 +246,24 @@ func TestAppendFormulaConstraints_injectedIntoResolve(t *testing.T) {
|
|||||||
TitleTemplate: title,
|
TitleTemplate: title,
|
||||||
DescriptionTemplate: desc,
|
DescriptionTemplate: desc,
|
||||||
})
|
})
|
||||||
if !strings.Contains(user, "Name: {{name}}") {
|
if !strings.Contains(user, "GPT predloga:") {
|
||||||
t.Fatalf("lost base template: %s", user)
|
t.Fatalf("expected A1 GPT predloga: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(user, "Title formula") || !strings.Contains(user, "attr [brand]") {
|
if !strings.Contains(user, "<name>{") || !strings.Contains(user, "brand") {
|
||||||
t.Fatalf("missing title formula: %s", user)
|
t.Fatalf("missing title formula in predloga: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(user, "Description formula") || !strings.Contains(user, "- p: Factual summary") {
|
if !strings.Contains(user, "<p>{Factual summary}</p>") {
|
||||||
t.Fatalf("missing description formula: %s", user)
|
t.Fatalf("missing description formula HTML: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(user, "SEO meta formula") || !strings.Contains(user, "meta_title (50-60 chars)") {
|
if !strings.Contains(user, "<metaDescription>{") || !strings.Contains(user, "120-155") {
|
||||||
t.Fatalf("missing SEO meta formula: %s", user)
|
t.Fatalf("missing SEO meta in predloga: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(sys, "SEO meta formula") {
|
if !strings.Contains(sys, "Si tekstopisec") {
|
||||||
t.Fatalf("system missing meta override: %s", sys)
|
t.Fatalf("system missing A1 copywriter: %s", sys)
|
||||||
}
|
}
|
||||||
// Render substitutes {{language}} in formula blocks.
|
// Render substitutes {{language}} in formula blocks.
|
||||||
_, rendered := RenderProductEnhancePrompts(
|
_, rendered := RenderProductEnhancePrompts(
|
||||||
"Write in {{language}}.", user,
|
sys, user,
|
||||||
"50", "Old Name", "", "", "", "sl",
|
"50", "Old Name", "", "", "", "sl",
|
||||||
map[string]any{"brand": "Vox", "product_model": "EHT6020"},
|
map[string]any{"brand": "Vox", "product_model": "EHT6020"},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,19 @@ func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string)
|
|||||||
systemTpl = strings.TrimSpace(in.EnhanceSystemTemplate)
|
systemTpl = strings.TrimSpace(in.EnhanceSystemTemplate)
|
||||||
userTpl = strings.TrimSpace(in.EnhanceUserTemplate)
|
userTpl = strings.TrimSpace(in.EnhanceUserTemplate)
|
||||||
catPrompt := strings.TrimSpace(in.CategoryEnhancePrompt)
|
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).
|
// Per-category prompt wins for the user message (company system keeps JSON schema / brand).
|
||||||
if catPrompt != "" {
|
if catPrompt != "" {
|
||||||
userTpl = ensureCategoryEnhanceUserContext(catPrompt)
|
userTpl = ensureCategoryEnhanceUserContext(catPrompt)
|
||||||
@@ -24,9 +37,6 @@ func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string)
|
|||||||
userTpl = def.UserTemplate
|
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.
|
// Category formulas are language-agnostic; inject once into the shared user skeleton.
|
||||||
userTpl = AppendFormulaConstraints(userTpl, in.TitleTemplate, descTpl, in.OmitSEOMeta)
|
userTpl = AppendFormulaConstraints(userTpl, in.TitleTemplate, descTpl, in.OmitSEOMeta)
|
||||||
// Free-form --- Title --- instructions when structured title_template is empty.
|
// Free-form --- Title --- instructions when structured title_template is empty.
|
||||||
|
|||||||
@@ -82,26 +82,18 @@ func TestResolveProductPromptTemplates_categoryWithTitleFormula(t *testing.T) {
|
|||||||
TitleTemplate: title,
|
TitleTemplate: title,
|
||||||
DescriptionTemplate: desc,
|
DescriptionTemplate: desc,
|
||||||
})
|
})
|
||||||
if !strings.Contains(user, "Emphasize energy class") {
|
// Structured formulas trigger A1-style GPT predloga (legacy generator.php behavior).
|
||||||
t.Fatalf("missing category prompt: %s", user)
|
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") {
|
if !strings.Contains(user, "GPT predloga:") {
|
||||||
t.Fatalf("missing category title framing: %s", user)
|
t.Fatalf("expected GPT predloga user: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(user, "Title formula") || !strings.Contains(user, "attr [brand]") {
|
if !strings.Contains(user, "Emphasize energy class") && !strings.Contains(user, "<name>") {
|
||||||
t.Fatalf("missing title formula: %s", user)
|
t.Fatalf("expected title/desc formula in predloga: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(user, "Description formula") || !strings.Contains(user, "- p: Factual summary") {
|
if !strings.Contains(user, "<p>{Factual summary}</p>") && !strings.Contains(user, "Factual summary") {
|
||||||
t.Fatalf("missing description formula: %s", user)
|
t.Fatalf("missing description formula HTML: %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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,20 +115,17 @@ func TestResolveProductPromptTemplates_derivesFormulaFromPromptHTML(t *testing.T
|
|||||||
// Empty structured template — production A1 path often only has prompt HTML.
|
// Empty structured template — production A1 path often only has prompt HTML.
|
||||||
DescriptionTemplate: nil,
|
DescriptionTemplate: nil,
|
||||||
})
|
})
|
||||||
if !strings.Contains(user, "Description formula") {
|
if !strings.Contains(sys, "Si tekstopisec") {
|
||||||
t.Fatalf("expected Description formula derived from prompt HTML:\n%s", user)
|
t.Fatalf("expected A1 copywriter system:\n%s", sys)
|
||||||
}
|
}
|
||||||
if !strings.Contains(user, "- h2:") || !strings.Contains(user, "- p:") || !strings.Contains(user, "- ul:") {
|
if !strings.Contains(user, "GPT predloga:") || !strings.Contains(user, "<name>{") {
|
||||||
t.Fatalf("expected h2/p/ul sections in formula:\n%s", user)
|
t.Fatalf("expected legacy GPT predloga with <name>:\n%s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(sys, "Description formula") {
|
if !strings.Contains(user, "<H2>{") && !strings.Contains(user, "<h2>{") {
|
||||||
t.Fatalf("missing description system override:\n%s", sys)
|
t.Fatalf("expected HTML description formula in predloga:\n%s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(sys, `"name" MUST be rewritten`) {
|
if strings.Contains(user, "Category guidance (applies to name") {
|
||||||
t.Fatalf("missing title rewrite system override:\n%s", sys)
|
t.Fatal("A1 path must not use soft category guidance overlay")
|
||||||
}
|
|
||||||
if !strings.Contains(user, "Title instructions (REQUIRED") {
|
|
||||||
t.Fatalf("missing Title section constraint:\n%s", user)
|
|
||||||
}
|
}
|
||||||
effective := aiprompts.EffectiveDescriptionTemplateAny(nil, prompt)
|
effective := aiprompts.EffectiveDescriptionTemplateAny(nil, prompt)
|
||||||
if descriptionSatisfiesFormula("plain Slovenian prose without tags", effective) {
|
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",
|
ReasoningEffort: "low",
|
||||||
})
|
})
|
||||||
elapsed := time.Since(started)
|
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 {
|
if err != nil {
|
||||||
// Network/provider failure vs parse failure after retry
|
// Network/provider failure vs parse failure after retry
|
||||||
name := preferredProductTitle(in.GTIN, in.Name, in.PriorProcessedName)
|
name := preferredProductTitle(in.GTIN, in.Name, in.PriorProcessedName)
|
||||||
|
|||||||
Reference in New Issue
Block a user