") {
+ t.Fatalf("fallback desc still has name tag: %v", obj["description"])
+ }
+}
diff --git a/apps/api/internal/processing/a1_enhance_smoke_test.go b/apps/api/internal/processing/a1_enhance_smoke_test.go
new file mode 100644
index 0000000..2cf07b9
--- /dev/null
+++ b/apps/api/internal/processing/a1_enhance_smoke_test.go
@@ -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" +
+ `{Napiši Novo ime izdelka in izpostavi en benefit}
` +
+ `{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}
` +
+ `{Izpostavi en benefit, in NE napiši Novo ime izdelka}
` +
+ `{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi tip izdelka lowercase}
` +
+ `{Tehnične specifikacije}- {Napiši 3-10 tehničnih specifikacij v alinejah}
` + "\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": "Zaščita z elegantnim videzom
" + strings.Repeat("Besedilo o ovitku. ", 20) +
+ "
Trden oprijem
" + strings.Repeat("Še več besedila. ", 20) +
+ "
- Barva: črna
- Model: Honor 200 Smart
",
+ "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, "{") {
+ 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, "{") && !strings.Contains(cap.user, "{") {
+ 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)
+ }
+}
diff --git a/apps/api/internal/processing/enhance_log_test.go b/apps/api/internal/processing/enhance_log_test.go
index 9f8c7d9..238d545 100644
--- a/apps/api/internal/processing/enhance_log_test.go
+++ b/apps/api/internal/processing/enhance_log_test.go
@@ -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, "{Naziv izdelka}
") {
+ 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, "{") || !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") {
diff --git a/apps/api/internal/processing/formula_prompt_test.go b/apps/api/internal/processing/formula_prompt_test.go
index ce3ed1c..9bcddd2 100644
--- a/apps/api/internal/processing/formula_prompt_test.go
+++ b/apps/api/internal/processing/formula_prompt_test.go
@@ -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, "{Heading}
") {
+ 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, "{") {
+ 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, "{") || !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, "{Factual summary}
") {
+ 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, "{") || !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"},
)
diff --git a/apps/api/internal/processing/prompt_render.go b/apps/api/internal/processing/prompt_render.go
index 02ffc8c..6314467 100644
--- a/apps/api/internal/processing/prompt_render.go
+++ b/apps/api/internal/processing/prompt_render.go
@@ -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
+ // //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 {…}
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.
diff --git a/apps/api/internal/processing/prompt_render_test.go b/apps/api/internal/processing/prompt_render_test.go
index fad5b1f..7d4abf7 100644
--- a/apps/api/internal/processing/prompt_render_test.go
+++ b/apps/api/internal/processing/prompt_render_test.go
@@ -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, "") {
+ 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, "{Factual summary}
") && !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, "{") {
+ t.Fatalf("expected legacy GPT predloga with :\n%s", user)
}
- if !strings.Contains(sys, "Description formula") {
- t.Fatalf("missing description system override:\n%s", sys)
+ if !strings.Contains(user, "{") && !strings.Contains(user, "{") {
+ 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) {
diff --git a/apps/api/internal/processing/steps.go b/apps/api/internal/processing/steps.go
index 0f55a29..72fccc7 100644
--- a/apps/api/internal/processing/steps.go
+++ b/apps/api/internal/processing/steps.go
@@ -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 / 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)