This commit is contained in:
2026-08-16 23:42:00 +02:00
parent 6e77154228
commit 92f046b542
32 changed files with 1443 additions and 105 deletions
+50 -24
View File
@@ -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,
+8 -2
View File
@@ -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}}")
+51
View File
@@ -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))
}
+79
View File
@@ -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")
}
}