fix
This commit is contained in:
@@ -26,7 +26,9 @@ type Variable struct {
|
||||
// Catalog of supported {{variables}} (only these are substituted; unknown tokens stay literal).
|
||||
var VariableCatalog = []Variable{
|
||||
{Name: "name", Label: "Product name", Description: "Current product title", Keys: []string{KeyProductEnhance, KeySEOMeta}},
|
||||
{Name: "description", Label: "Description", Description: "Current product description", Keys: []string{KeyProductEnhance, KeySEOMeta}},
|
||||
// description = product HTML body input (not SEO meta_description). Template key
|
||||
// stays "description" so stored tenant prompts keep working (renaming would be BREAKING).
|
||||
{Name: "description", Label: "Product description", Description: "Current product description (HTML body). Not SEO meta_description.", Keys: []string{KeyProductEnhance, KeySEOMeta}},
|
||||
{Name: "category", Label: "Category", Description: "Resolved category name", Keys: []string{KeyProductEnhance, KeySEOMeta}},
|
||||
{Name: "attrs", Label: "Attributes", Description: "Compact JSON of product attributes", Keys: []string{KeyProductEnhance}},
|
||||
{Name: "gtin", Label: "GTIN", Description: "Product GTIN / barcode when present", Keys: []string{KeyProductEnhance}},
|
||||
@@ -48,20 +50,38 @@ type DefaultTemplate struct {
|
||||
}
|
||||
|
||||
// CategoryEnhanceUserTemplate is the shared per-category (and built-in) enhance USER
|
||||
// message. Includes {{name}} {{description}} {{attrs}} {{category}} {{language}}.
|
||||
// Compatible with the enhance system JSON schema {"name","description"} — not a
|
||||
// competing HTML marketing document. Title/description formulas stay in
|
||||
// title_template / description_template and are appended at render time as plain
|
||||
// text instructions (see processing.AppendFormulaConstraints).
|
||||
// Used by local A1/Demo prompt repair and seed-a1 overlays.
|
||||
const CategoryEnhanceUserTemplate = `Your reply is parsed as JSON {"name":"string","description":"string"} only (system schema). Write name and description in {{language}} (do not hardcode a language).
|
||||
- name: short retail title; follow any Title formula constraints that follow; use Attrs
|
||||
- description: when a Description formula follows, emit ONE HTML string covering each section in order (tags matching type: h1/h2/h3/h4, p, ul); otherwise prefer 1-3 factual paragraphs as ONE string with limited HTML (<h2><p><ul><li>) — do NOT emit a competing full HTML document, <name>/<metaDescription> blocks, or separate schema
|
||||
// message, sectioned by role (title / description / meta / attributes) using the
|
||||
// same "--- Section ---" markers as legacy enhance-product. Includes {{name}}
|
||||
// {{description}} {{attrs}} {{category}} {{language}}. Compatible with enhance
|
||||
// JSON {"name","description","meta_title","meta_description","attrs"} —
|
||||
// description is formula HTML product body; meta_* are plain SEO fields (legacy
|
||||
// A1 <metaDescription> / cats.json metaTitle+metaDescription); attrs is the
|
||||
// category-allowlisted attribute map. Title/description/meta formulas stay in
|
||||
// title_template / description_template and are appended at render time (see
|
||||
// processing.AppendFormulaConstraints / AppendAttributeConstraints). Categorize
|
||||
// (unique_id) is RoleCategorize / ProductCategorize* — not this overlay. Used by
|
||||
// local A1/Demo prompt repair and seed-a1 overlays.
|
||||
const CategoryEnhanceUserTemplate = `Your reply is parsed as JSON {"name":"string","description":"string","meta_title":"string","meta_description":"string","attrs":{}} only (system schema). Write all string fields in {{language}} (do not hardcode a language).
|
||||
|
||||
Category: {{category}}
|
||||
--- Title ---
|
||||
Role: title. Build JSON "name": short retail title; follow any Title formula constraints that follow; use Attrs.
|
||||
Name: {{name}}
|
||||
Desc: {{description}}
|
||||
Attrs: {{attrs}}`
|
||||
--- End Title ---
|
||||
|
||||
--- Description ---
|
||||
Role: description. Build JSON "description": product body HTML only (not SEO meta). When a Description formula follows, emit ONE HTML string covering each section in order (tags matching type: h1/h2/h3/h4, p, ul); otherwise prefer 1-3 factual paragraphs as ONE string with limited HTML (<h2><p><ul><li>) — do NOT emit a competing full HTML document or wrap the whole reply in <name>/<metaDescription> tags.
|
||||
Description: {{description}}
|
||||
--- End Description ---
|
||||
|
||||
--- Meta ---
|
||||
Role: meta. Build JSON "meta_title" and "meta_description" as plain SEO text (never HTML). meta_title: 50-60 chars; meta_description: 120-155 chars; follow any SEO meta formula that follows; never copy the full description HTML into meta_description.
|
||||
--- End Meta ---
|
||||
|
||||
--- Attributes ---
|
||||
Role: attributes. Build JSON "attrs" as an object of attribute_key → value strings. Prefer Allowed attribute keys / Title formula attr slots that follow; remap near-miss labels onto those keys; fill missing keys only from Name/Description/Category/Attrs evidence; never invent specs; omit unknown keys; never invent dimensions.
|
||||
Category: {{category}}
|
||||
Attrs: {{attrs}}
|
||||
--- End Attributes ---`
|
||||
|
||||
// CategoryEnhancePromptNeedsRepair reports whether a stored categories.prompt value
|
||||
// should be replaced by CategoryEnhanceUserTemplate (idempotent equality check).
|
||||
@@ -77,38 +97,44 @@ func CategoryEnhancePromptNeedsRepair(prompt string) bool {
|
||||
var BuiltInDefaults = []DefaultTemplate{
|
||||
{
|
||||
Key: KeyProductEnhance,
|
||||
Label: "Product title & description",
|
||||
Description: "Used when processing products (AI enhance step). Categorize (taxonomy pick when category is empty) is a separate pipeline step before this.",
|
||||
Label: "Product title, description, SEO meta & attributes",
|
||||
Description: "Used when processing products (AI enhance step). Produces title, HTML description, meta_*, and attrs. Categorize (taxonomy pick when category is empty) is a separate pipeline step before this.",
|
||||
SystemTemplate: `Retail product copywriter.
|
||||
Rules:
|
||||
- Reply with ONLY JSON (no markdown)
|
||||
- Schema: {"name":"string","description":"string"}
|
||||
- Schema: {"name":"string","description":"string","meta_title":"string","meta_description":"string","attrs":{}}
|
||||
- name: short retail title
|
||||
- description: follow any Description formula in the user message (structured HTML sections); otherwise 1-3 factual paragraphs; never copy name as description
|
||||
- When Desc is empty or the same as Name, write from Category and Attrs only (do not invent specs); still obey a Description formula when present
|
||||
- Write name and description in {{language}}
|
||||
- description: product body HTML only — follow any Description formula in the user message (structured HTML sections); otherwise 1-3 factual paragraphs; never copy name as description; never put SEO meta here
|
||||
- meta_title: plain SEO title 50-60 chars (follow any SEO meta formula)
|
||||
- meta_description: plain SEO snippet 120-155 chars (follow any SEO meta formula); never HTML
|
||||
- attrs: object of attribute_key → value; only Allowed attribute keys / formula attr slots from the user message; remap near-miss labels; fill gaps from evidence only; omit unknowns; do not invent specs
|
||||
- When Description is empty or the same as Name, write description from Category and Attrs only (do not invent specs); still obey a Description formula when present
|
||||
- Write name, description, meta_title, and meta_description in {{language}}
|
||||
Example:
|
||||
{"name":"Acme Widget Pro","description":"Durable widget for everyday use. Clear specs, ready to ship."}
|
||||
{"name":"Acme Widget Pro","description":"<p>Durable widget for everyday use. Clear specs, ready to ship.</p>","meta_title":"Acme Widget Pro | Durable Daily Use","meta_description":"Shop Acme Widget Pro for reliable everyday performance. Clear specs and fast delivery.","attrs":{"brand":"Acme","product_model":"Widget Pro"}}
|
||||
{{brand_voice}}`,
|
||||
UserTemplate: CategoryEnhanceUserTemplate,
|
||||
},
|
||||
{
|
||||
Key: KeySEOMeta,
|
||||
Label: "SEO meta title & description",
|
||||
Description: "Used when applying AI SEO meta to a product.",
|
||||
Description: "Used when applying AI SEO meta to a product (standalone path). Product enhance already emits meta_* when run.",
|
||||
SystemTemplate: `SEO meta writer for ecommerce.
|
||||
Rules:
|
||||
- Reply with ONLY JSON (no markdown)
|
||||
- Schema: {"meta_title":"string","meta_description":"string"}
|
||||
- meta_title: 50-60 chars, product + benefit
|
||||
- meta_description: 120-155 chars, factual
|
||||
- meta_description: 120-155 chars, factual plain text (not HTML product description)
|
||||
- Write meta_title and meta_description in {{language}}
|
||||
Example:
|
||||
{"meta_title":"Acme Widget Pro | Durable Daily Use","meta_description":"Shop Acme Widget Pro for reliable everyday performance. Clear specs and fast delivery."}
|
||||
{{brand_voice}}`,
|
||||
UserTemplate: `Name: {{name}}
|
||||
UserTemplate: `--- Meta ---
|
||||
Role: meta. Reply with JSON meta_title and meta_description only (standalone seo_meta path).
|
||||
Name: {{name}}
|
||||
Category: {{category}}
|
||||
Desc: {{description}}`,
|
||||
Description: {{description}}
|
||||
--- End Meta ---`,
|
||||
},
|
||||
{
|
||||
Key: KeyCampaignEmail,
|
||||
|
||||
@@ -22,8 +22,14 @@ func TestCategoryEnhanceUserTemplateHasRequiredVars(t *testing.T) {
|
||||
if strings.Contains(lower, "100 besed") || strings.Contains(lower, "gpt predloga") {
|
||||
t.Fatal("template must not demand legacy long HTML marketing docs")
|
||||
}
|
||||
if !strings.Contains(CategoryEnhanceUserTemplate, `{"name":"string","description":"string"}`) {
|
||||
t.Fatal("template should reference JSON schema shape")
|
||||
if !strings.Contains(CategoryEnhanceUserTemplate, `{"name":"string","description":"string","meta_title":"string","meta_description":"string","attrs":{}}`) {
|
||||
t.Fatal("template should reference enhance JSON schema including meta_* and attrs")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(CategoryEnhanceUserTemplate), `build json "attrs"`) {
|
||||
t.Fatal("Attributes role must ask for JSON attrs extraction/enhancement")
|
||||
}
|
||||
if !CategoryEnhanceHasRoleSections(CategoryEnhanceUserTemplate) {
|
||||
t.Fatal("canonical template must be role-sectioned for title/description/meta/attributes")
|
||||
}
|
||||
if !strings.Contains(lower, "{{language}}") {
|
||||
t.Fatal("language must come from {{language}}")
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package aiprompts
|
||||
|
||||
import "strings"
|
||||
|
||||
// Pipeline prompt roles mirror legacy Descrybe steps (categorize → title →
|
||||
// description[+meta] → attributes). Categorize stays a separate Completer call
|
||||
// (ProductCategorize*). category.prompt (CategoryEnhanceUserTemplate) is the
|
||||
// shared enhance USER overlay, sectioned for title/description/meta/attrs so
|
||||
// seed/repair and title/meta/attrs formula work (AppendFormulaConstraints) share
|
||||
// one skeleton. Enhance JSON includes meta_* (legacy bundled meta); KeySEOMeta
|
||||
// remains the standalone SEO path.
|
||||
const (
|
||||
RoleCategorize = "categorize" // pick taxonomy unique_id (processing.ProductCategorize*)
|
||||
RoleTitle = "title" // JSON name / title formula
|
||||
RoleDescription = "description" // JSON description / description formula
|
||||
RoleMeta = "meta" // JSON meta_* in enhance + KeySEOMeta standalone
|
||||
RoleAttributes = "attributes" // JSON attrs object + allowlist/formula key guidance
|
||||
)
|
||||
|
||||
// Section markers match legacy enhance-product "--- Section ---" / "--- Meta Title ---"
|
||||
// style so stored category prompts stay human-readable and machine-detectable.
|
||||
const (
|
||||
SectionTitleStart = "--- Title ---"
|
||||
SectionTitleEnd = "--- End Title ---"
|
||||
SectionDescriptionStart = "--- Description ---"
|
||||
SectionDescriptionEnd = "--- End Description ---"
|
||||
SectionMetaStart = "--- Meta ---"
|
||||
SectionMetaEnd = "--- End Meta ---"
|
||||
SectionAttributesStart = "--- Attributes ---"
|
||||
SectionAttributesEnd = "--- End Attributes ---"
|
||||
)
|
||||
|
||||
// CategoryPromptRoles lists the role ids used across categorize + enhance + SEO.
|
||||
var CategoryPromptRoles = []string{
|
||||
RoleCategorize,
|
||||
RoleTitle,
|
||||
RoleDescription,
|
||||
RoleMeta,
|
||||
RoleAttributes,
|
||||
}
|
||||
|
||||
// CategoryEnhanceHasRoleSections reports whether a stored categories.prompt
|
||||
// value uses the structured title/description/meta/attributes section markers
|
||||
// from CategoryEnhanceUserTemplate (seed/repair canonical shape).
|
||||
func CategoryEnhanceHasRoleSections(prompt string) bool {
|
||||
lower := strings.ToLower(prompt)
|
||||
return strings.Contains(lower, strings.ToLower(SectionTitleStart)) &&
|
||||
strings.Contains(lower, strings.ToLower(SectionDescriptionStart)) &&
|
||||
strings.Contains(lower, strings.ToLower(SectionMetaStart)) &&
|
||||
strings.Contains(lower, strings.ToLower(SectionAttributesStart))
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package aiprompts
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCategoryEnhanceHasRoleSections(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !CategoryEnhanceHasRoleSections(CategoryEnhanceUserTemplate) {
|
||||
t.Fatal("canonical CategoryEnhanceUserTemplate must have title/description/meta/attributes sections")
|
||||
}
|
||||
if CategoryEnhanceHasRoleSections("legacy HTML prompt") {
|
||||
t.Fatal("unstructured prompt must not report role sections")
|
||||
}
|
||||
if CategoryEnhanceHasRoleSections("--- Title ---\nonly title") {
|
||||
t.Fatal("partial sections must not pass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategoryPromptRolesOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := []string{RoleCategorize, RoleTitle, RoleDescription, RoleMeta, RoleAttributes}
|
||||
if len(CategoryPromptRoles) != len(want) {
|
||||
t.Fatalf("roles len=%d want %d", len(CategoryPromptRoles), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
if CategoryPromptRoles[i] != w {
|
||||
t.Fatalf("roles[%d]=%q want %q", i, CategoryPromptRoles[i], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategoryEnhanceUserTemplateRoleMarkers(t *testing.T) {
|
||||
t.Parallel()
|
||||
tpl := CategoryEnhanceUserTemplate
|
||||
for _, marker := range []string{
|
||||
SectionTitleStart, SectionTitleEnd,
|
||||
SectionDescriptionStart, SectionDescriptionEnd,
|
||||
SectionMetaStart, SectionMetaEnd,
|
||||
SectionAttributesStart, SectionAttributesEnd,
|
||||
} {
|
||||
if !strings.Contains(tpl, marker) {
|
||||
t.Fatalf("template missing %q", marker)
|
||||
}
|
||||
}
|
||||
lower := strings.ToLower(tpl)
|
||||
for _, role := range []string{"role: title", "role: description", "role: meta", "role: attributes"} {
|
||||
if !strings.Contains(lower, role) {
|
||||
t.Fatalf("template missing %q", role)
|
||||
}
|
||||
}
|
||||
// HeuristicCompleter / labeledPromptValue depend on these product context labels.
|
||||
for _, label := range []string{"Name: {{name}}", "Description: {{description}}", "Category: {{category}}", "Attrs: {{attrs}}"} {
|
||||
if !strings.Contains(tpl, label) {
|
||||
t.Fatalf("template missing product label %q", label)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(tpl, "meta_title") || !strings.Contains(tpl, "meta_description") {
|
||||
t.Fatal("template must keep enhance meta_* schema (parallel title/meta work)")
|
||||
}
|
||||
if !strings.Contains(tpl, `"attrs":{}`) {
|
||||
t.Fatal("template must keep enhance attrs schema (parallel attributes work)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSEOMetaUserTemplateHasMetaSection(t *testing.T) {
|
||||
t.Parallel()
|
||||
def, ok := DefaultFor(KeySEOMeta)
|
||||
if !ok {
|
||||
t.Fatal("missing seo_meta default")
|
||||
}
|
||||
if !strings.Contains(def.UserTemplate, SectionMetaStart) {
|
||||
t.Fatal("seo_meta user template should use --- Meta --- section")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(def.UserTemplate), "role: meta") {
|
||||
t.Fatal("seo_meta user template should declare Role: meta")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user