- Category enhance prompts (Sync A1 / seed-a1 / repair) now use the same three role sections as the rest of the platform: Title / Description / Meta. The retired "--- Attributes ---" role section is removed from the canonical template, the legacy-split overlay, and the web prompt editor; Category/Attrs stay as plain product-context lines (attribute extraction remains pipeline-level via AppendAttributeConstraints). - Stored prompts still carrying an attributes section are detected by CategoryEnhancePromptNeedsRepair and rewritten on the next Sync. - Legacy wp_product_categories.sql splits now also seed a default Slovenian metaTitle rule (dumps only carried <metaDescription>), so every A1 category gets all four prompt areas: title formula, description sections, meta title, meta description. - Role-sectioned prompts no longer imply the A1 SEO-omit cohort (CompanyOmitsSEOMeta): sectioned prompts are the canonical prompt-editor output for every tenant, and the sniff silently disabled SEO meta for any company that saved a category prompt. - Verified locally: repair-category-prompts -apply updated 238 A1 + Platform Demo categories from scripts/seed/wp_product_categories.sql (216 legacy splits), idempotent on re-run, zero attributes sections left in DB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
193 lines
9.9 KiB
Go
193 lines
9.9 KiB
Go
package aiprompts
|
|
|
|
import "strings"
|
|
|
|
// Prompt keys stored in ai_prompt_templates.prompt_key.
|
|
const (
|
|
KeyProductEnhance = "product_enhance"
|
|
KeySEOMeta = "seo_meta"
|
|
KeyCampaignEmail = "campaign_email"
|
|
)
|
|
|
|
// MaxSystemRunes / MaxUserRunes bound stored templates (prompt-injection surface).
|
|
const (
|
|
MaxSystemRunes = 6000
|
|
MaxUserRunes = 4000
|
|
)
|
|
|
|
// Variable describes a placeholder tenants can insert into templates.
|
|
type Variable struct {
|
|
Name string `json:"name"`
|
|
Label string `json:"label"`
|
|
Description string `json:"description"`
|
|
Keys []string `json:"keys"` // prompt_key values that support this variable
|
|
}
|
|
|
|
// 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}},
|
|
// 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}},
|
|
{Name: "brand", Label: "Brand name", Description: "Company or product brand label", Keys: []string{KeySEOMeta, KeyCampaignEmail}},
|
|
{Name: "brand_voice", Label: "Brand voice", Description: "Brand kit tone / dos / don'ts block", Keys: []string{KeyProductEnhance, KeySEOMeta, KeyCampaignEmail}},
|
|
{Name: "language", Label: "Content language", Description: "Company content language (English display name)", Keys: []string{KeyProductEnhance, KeySEOMeta, KeyCampaignEmail}},
|
|
{Name: "campaign_prompt", Label: "Campaign brief", Description: "Per-campaign user brief or template default", Keys: []string{KeyCampaignEmail}},
|
|
{Name: "products", Label: "Product list", Description: "Plain-text product snippets for the campaign", Keys: []string{KeyCampaignEmail}},
|
|
{Name: "template_key", Label: "Template key", Description: "Campaign template id (christmas, custom, …)", Keys: []string{KeyCampaignEmail}},
|
|
}
|
|
|
|
// DefaultTemplate is the built-in prompt when the company has no custom row or disabled it.
|
|
type DefaultTemplate struct {
|
|
Key string `json:"key"`
|
|
Label string `json:"label"`
|
|
Description string `json:"description"`
|
|
SystemTemplate string `json:"system_template"`
|
|
UserTemplate string `json:"user_template"`
|
|
}
|
|
|
|
// CategoryEnhanceUserTemplate is the shared per-category (and built-in) enhance USER
|
|
// message, sectioned by role (title / description / meta) 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). Attribute
|
|
// extraction is pipeline-level (allowlist via AppendAttributeConstraints) and is
|
|
// deliberately NOT a category prompt section; Category/Attrs stay as plain
|
|
// product-context lines. Title/description/meta formulas stay in
|
|
// title_template / description_template and are appended at render time (see
|
|
// processing.AppendFormulaConstraints). 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).
|
|
|
|
--- Title ---
|
|
` + TitleRoleInstruction + `
|
|
Name: {{name}}
|
|
--- 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 ---
|
|
|
|
Product context:
|
|
Category: {{category}}
|
|
Attrs: {{attrs}}`
|
|
|
|
// CategoryEnhancePromptNeedsRepair reports whether a stored categories.prompt value
|
|
// should be rewritten into role-sectioned enhance overlay form. Empty prompts are
|
|
// left alone. Canonical CategoryEnhanceUserTemplate and per-category overlays that
|
|
// already carry Title/Description/Meta markers are OK — equality with the shared
|
|
// template is not required (legacy splits keep Slovenian rules). Prompts still
|
|
// carrying a retired "--- Attributes ---" role section are rewritten so attribute
|
|
// prompting stays out of category prompts.
|
|
func CategoryEnhancePromptNeedsRepair(prompt string) bool {
|
|
p := strings.TrimSpace(prompt)
|
|
if p == "" {
|
|
return false
|
|
}
|
|
if IsLegacyCombinedEnhancePrompt(p) {
|
|
return true
|
|
}
|
|
if !CategoryEnhanceHasRoleSections(p) {
|
|
return true
|
|
}
|
|
return strings.Contains(strings.ToLower(p), strings.ToLower(SectionAttributesStart))
|
|
}
|
|
|
|
// BuiltInDefaults match the previous hardcoded system prompts, with structured user templates.
|
|
var BuiltInDefaults = []DefaultTemplate{
|
|
{
|
|
Key: KeyProductEnhance,
|
|
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","meta_title":"string","meta_description":"string","attrs":{}}
|
|
- name: short retail title — never brand-only; follow any Title formula (type + brand + model); use Attrs
|
|
- 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":"<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 (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 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: `--- Meta ---
|
|
Role: meta. Reply with JSON meta_title and meta_description only (standalone seo_meta path).
|
|
Name: {{name}}
|
|
Category: {{category}}
|
|
Description: {{description}}
|
|
--- End Meta ---`,
|
|
},
|
|
{
|
|
Key: KeyCampaignEmail,
|
|
Label: "Campaign email",
|
|
Description: "Used when generating marketing emails with AI.",
|
|
SystemTemplate: `Marketing email writer.
|
|
Rules:
|
|
- Reply with ONLY JSON (no markdown)
|
|
- Schema: {"subject":"string","html_body":"string","plain_body":"string"}
|
|
- subject: short
|
|
- html_body: simple HTML (<p>, <ul>, <a> only)
|
|
- plain_body: plain text mirror
|
|
- Write subject, html_body, and plain_body in {{language}}
|
|
Example:
|
|
{"subject":"Holiday picks from Acme","html_body":"<p>Season's greetings.</p><ul><li>Widget Pro</li></ul><p><a href=\"#\">Shop now</a></p>","plain_body":"Season's greetings.\n- Widget Pro\nShop now"}
|
|
{{brand_voice}}`,
|
|
UserTemplate: `{{campaign_prompt}}
|
|
|
|
Products:
|
|
{{products}}
|
|
Brand: {{brand}}
|
|
Template: {{template_key}}`,
|
|
},
|
|
}
|
|
|
|
// ValidPromptKey reports whether key is a known prompt_key.
|
|
func ValidPromptKey(key string) bool {
|
|
switch key {
|
|
case KeyProductEnhance, KeySEOMeta, KeyCampaignEmail:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// DefaultFor returns the built-in default for key, or empty if unknown.
|
|
func DefaultFor(key string) (DefaultTemplate, bool) {
|
|
for _, d := range BuiltInDefaults {
|
|
if d.Key == key {
|
|
return d, true
|
|
}
|
|
}
|
|
return DefaultTemplate{}, false
|
|
}
|