This commit is contained in:
2026-08-23 16:34:50 +02:00
parent 815e26d359
commit a30b78c3b6
8 changed files with 178 additions and 33 deletions
@@ -161,30 +161,64 @@ func parseDescriptionFormula(template any) (DescriptionFormula, bool) {
} }
} }
// EffectiveDescriptionFormula returns the stored description_template when it has // EffectiveDescriptionFormula merges structured description_template with the
// sections; otherwise derives sections from the category enhance prompt Description // category enhance prompt Title/Description/Meta role sections.
// role (legacy HTML <H2>{…}</H2><p>{…}</p> overlays). That is how A1 and tenants //
// author HTML structure in /categories/.../prompt — without this, enhance accepts // Precedence for HTML body sections:
// plain prose and never runs the formula quality gate. // 1. Prompt Description HTML overlay when it yields more sections than stored
// (tenants author structure in /categories/.../prompt Section instructions).
// 2. Otherwise stored description_template.sections.
//
// Meta instructions: stored metaTitle/metaDescription win; else --- Meta --- body.
func EffectiveDescriptionFormula(stored any, categoryPrompt string) DescriptionFormula { func EffectiveDescriptionFormula(stored any, categoryPrompt string) DescriptionFormula {
if f, ok := parseDescriptionFormula(stored); ok && len(f.Sections) > 0 { storedF, storedOK := parseDescriptionFormula(stored)
return f out := DescriptionFormula{}
if storedOK {
out = storedF
} }
body := ExtractEnhanceSectionBody(categoryPrompt, SectionDescriptionStart, SectionDescriptionEnd)
if body == "" { descBody := ExtractEnhanceSectionBody(categoryPrompt, SectionDescriptionStart, SectionDescriptionEnd)
body = strings.TrimSpace(categoryPrompt) if descBody == "" && !promptHasRoleMarkers(categoryPrompt) {
// Unsectioned prompt may still be a legacy HTML blob.
descBody = strings.TrimSpace(categoryPrompt)
} }
if body == "" || !reLegacyHTMLBlock.MatchString(body) { var derived DescriptionFormula
if f, ok := parseDescriptionFormula(stored); ok { if descBody != "" && reLegacyHTMLBlock.MatchString(descBody) {
return f derived = DeriveDescriptionFormulaFromLegacyParts(LegacyEnhanceParts{
} DescriptionRules: descBody,
return DescriptionFormula{}
}
return DeriveDescriptionFormulaFromLegacyParts(LegacyEnhanceParts{
DescriptionRules: body,
WasLegacy: true, WasLegacy: true,
}) })
} }
if len(derived.Sections) > len(out.Sections) {
out.Sections = derived.Sections
}
metaBody := ExtractEnhanceSectionBody(categoryPrompt, SectionMetaStart, SectionMetaEnd)
if strings.TrimSpace(out.MetaTitle) == "" && strings.TrimSpace(out.MetaDescription) == "" && strings.TrimSpace(metaBody) != "" {
out.MetaDescription = strings.TrimSpace(metaBody)
out.MetaTitle = DefaultLegacyMetaTitleRule
} else {
if strings.TrimSpace(out.MetaTitle) == "" && strings.TrimSpace(derived.MetaTitle) != "" {
out.MetaTitle = derived.MetaTitle
}
if strings.TrimSpace(out.MetaDescription) == "" && strings.TrimSpace(derived.MetaDescription) != "" {
out.MetaDescription = derived.MetaDescription
}
}
return out
}
func promptHasRoleMarkers(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))
}
// TitleSectionInstructions returns the --- Title --- body from a category prompt.
func TitleSectionInstructions(categoryPrompt string) string {
return ExtractEnhanceSectionBody(categoryPrompt, SectionTitleStart, SectionTitleEnd)
}
// 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).
@@ -207,13 +241,18 @@ func EffectiveDescriptionTemplateAny(stored any, categoryPrompt string) any {
// CategoryTitlePromptRequiresRewrite reports whether the Title role instructs a // CategoryTitlePromptRequiresRewrite reports whether the Title role instructs a
// new retail name (not keep/echo supplier title). Used to reject "Matched" copy-paste. // new retail name (not keep/echo supplier title). Used to reject "Matched" copy-paste.
func CategoryTitlePromptRequiresRewrite(categoryPrompt string) bool { func CategoryTitlePromptRequiresRewrite(categoryPrompt string) bool {
body := ExtractEnhanceSectionBody(categoryPrompt, SectionTitleStart, SectionTitleEnd) body := TitleSectionInstructions(categoryPrompt)
if body == "" { if body == "" {
body = strings.TrimSpace(categoryPrompt) body = strings.TrimSpace(categoryPrompt)
} }
if body == "" { if body == "" {
return false return false
} }
// Built-in CategoryEnhanceUserTemplate Title role mentions "Title formula"
// generically — that is not a tenant rewrite formula.
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(body)), "role: title") {
return false
}
lower := strings.ToLower(body) lower := strings.ToLower(body)
cues := []string{ cues := []string{
"napiši novo ime", "napiši novo ime",
@@ -223,8 +262,9 @@ func CategoryTitlePromptRequiresRewrite(categoryPrompt string) bool {
"write a new", "write a new",
"new product name", "new product name",
"rename", "rename",
"title formula",
"sentence case", "sentence case",
"tip izdelka",
"product type",
} }
for _, c := range cues { for _, c := range cues {
if strings.Contains(lower, c) { if strings.Contains(lower, c) {
@@ -93,6 +93,40 @@ func TestEffectiveDescriptionFormula_fromPromptDescriptionSection(t *testing.T)
} }
} }
func TestEffectiveDescriptionFormula_prefersRicherPromptHTML(t *testing.T) {
t.Parallel()
stored := map[string]any{
"sections": []any{
map[string]any{"type": "p", "instructions": "Short"},
},
"metaTitle": "Stored meta title rule",
}
prompt := SectionDescriptionStart + "\n" +
`<H2>{One}</H2><p>{Two}</p><H2>{Three}</H2><p>{Four}</p>` +
`<b>{Specs}</b><ul><li>{Bullets}</li></ul>` + "\n" +
SectionDescriptionEnd + "\n" +
SectionMetaStart + "\nIgnored because stored meta exists\n" + SectionMetaEnd
f := EffectiveDescriptionFormula(stored, prompt)
if len(f.Sections) < 4 {
t.Fatalf("want prompt HTML sections to win over thin stored, got %#v", f.Sections)
}
if f.MetaTitle != "Stored meta title rule" {
t.Fatalf("stored metaTitle should win, got %q", f.MetaTitle)
}
}
func TestEffectiveDescriptionFormula_metaSectionWhenStoredEmpty(t *testing.T) {
t.Parallel()
prompt := SectionMetaStart + "\nWrite meta_description to 140 chars.\n" + SectionMetaEnd
f := EffectiveDescriptionFormula(nil, prompt)
if !strings.Contains(f.MetaDescription, "140") {
t.Fatalf("metaDescription=%q", f.MetaDescription)
}
if strings.TrimSpace(f.MetaTitle) == "" {
t.Fatal("expected default metaTitle when Meta section present")
}
}
func TestExtractEnhanceSectionBody(t *testing.T) { func TestExtractEnhanceSectionBody(t *testing.T) {
t.Parallel() t.Parallel()
prompt := SectionTitleStart + "\nHello\n" + SectionTitleEnd + "\n" + prompt := SectionTitleStart + "\nHello\n" + SectionTitleEnd + "\n" +
+14 -11
View File
@@ -140,27 +140,30 @@ func ShouldRefuseEnhanceHashSkip(priorDesc string, titles ...string) bool {
// DescriptionMissingFormulaHTMLTags is true when sectionTypes require HTML tags // DescriptionMissingFormulaHTMLTags is true when sectionTypes require HTML tags
// (h1/h2/h3/h4, p, ul) that are absent from desc — formula-mismatch for skip/clear. // (h1/h2/h3/h4, p, ul) that are absent from desc — formula-mismatch for skip/clear.
// Counts matter: a formula with two h2 sections needs at least two <h2 opens.
func DescriptionMissingFormulaHTMLTags(desc string, sectionTypes []string) bool { func DescriptionMissingFormulaHTMLTags(desc string, sectionTypes []string) bool {
if len(sectionTypes) == 0 { if len(sectionTypes) == 0 {
return false return false
} }
lower := strings.ToLower(desc) need := map[string]int{}
for _, raw := range sectionTypes { for _, raw := range sectionTypes {
typ := strings.ToLower(strings.TrimSpace(raw)) typ := strings.ToLower(strings.TrimSpace(raw))
switch typ { switch typ {
case "h1", "h2", "h3", "h4": case "h1", "h2", "h3", "h4", "ul":
if !strings.Contains(lower, "<"+typ) { need[typ]++
return true
}
case "ul":
if !strings.Contains(lower, "<ul") {
return true
}
case "p", "": case "p", "":
if !strings.Contains(lower, "<p") { need["p"]++
return true
} }
} }
if len(need) == 0 {
return false
}
lower := strings.ToLower(desc)
for typ, n := range need {
needle := "<" + typ
if strings.Count(lower, needle) < n {
return true
}
} }
return false return false
} }
@@ -48,4 +48,12 @@ func TestDescriptionMissingFormulaHTMLTags(t *testing.T) {
if DescriptionMissingFormulaHTMLTags("<h1>T</h1><p>Body</p><ul><li>x</li></ul>", types) { if DescriptionMissingFormulaHTMLTags("<h1>T</h1><p>Body</p><ul><li>x</li></ul>", types) {
t.Fatal("full HTML must match") t.Fatal("full HTML must match")
} }
twoH2 := []string{"h2", "p", "h2", "p", "ul"}
if !DescriptionMissingFormulaHTMLTags("<h2>A</h2><p>One</p><ul><li>x</li></ul>", twoH2) {
t.Fatal("single h2 must fail when formula needs two")
}
ok := "<h2>A</h2><p>One</p><h2>B</h2><p>Two</p><ul><li>x</li></ul>"
if DescriptionMissingFormulaHTMLTags(ok, twoH2) {
t.Fatal("two h2 + two p + ul should satisfy")
}
} }
@@ -47,6 +47,46 @@ func AppendFormulaConstraints(userTpl string, titleTemplate, descriptionTemplate
return userTpl + "\n\n" + joined.String() return userTpl + "\n\n" + joined.String()
} }
// FormatTitleSectionConstraint turns free-form --- Title --- section instructions
// into a required enhance constraint when no structured title_template elements exist.
func FormatTitleSectionConstraint(categoryPrompt string) string {
body := strings.TrimSpace(aiprompts.TitleSectionInstructions(categoryPrompt))
if body == "" || isBuiltInTitleRoleBoilerplate(body) {
return ""
}
var b strings.Builder
b.WriteString("Title instructions (REQUIRED — overrides any shorter \"short retail title\" rule):\n")
b.WriteString(body)
b.WriteString("\nBuild JSON \"name\" exactly per these instructions in {{language}}.")
b.WriteString(" Never copy the supplier Name unchanged when instructions ask for a new name or formula.")
return b.String()
}
func isBuiltInTitleRoleBoilerplate(body string) bool {
lower := strings.ToLower(strings.TrimSpace(body))
return strings.HasPrefix(lower, "role: title")
}
// AppendTitleSectionConstraint appends FormatTitleSectionConstraint when the
// category Title role has text and structured title_template is empty/unusable.
func AppendTitleSectionConstraint(userTpl, categoryPrompt string, titleTemplate any) string {
if FormatTitleFormulaConstraint(titleTemplate) != "" {
return strings.TrimSpace(userTpl)
}
block := FormatTitleSectionConstraint(categoryPrompt)
if block == "" {
return strings.TrimSpace(userTpl)
}
userTpl = strings.TrimSpace(userTpl)
if strings.Contains(userTpl, "Title instructions (REQUIRED") {
return userTpl
}
if userTpl == "" {
return block
}
return userTpl + "\n\n" + block
}
// MaxAttrAllowlistPromptKeys caps Allowed attribute keys listed in enhance prompts. // MaxAttrAllowlistPromptKeys caps Allowed attribute keys listed in enhance prompts.
const MaxAttrAllowlistPromptKeys = 40 const MaxAttrAllowlistPromptKeys = 40
@@ -29,6 +29,8 @@ func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string)
descTpl := aiprompts.EffectiveDescriptionTemplateAny(in.DescriptionTemplate, catPrompt) 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.
userTpl = AppendTitleSectionConstraint(userTpl, catPrompt, in.TitleTemplate)
// Category attribute allowlist + title-formula keys guide JSON "attrs" extraction. // Category attribute allowlist + title-formula keys guide JSON "attrs" extraction.
allowed := enhanceAllowedAttrKeys(in, in.CategoryUniqueID) allowed := enhanceAllowedAttrKeys(in, in.CategoryUniqueID)
userTpl = AppendAttributeConstraints(userTpl, allowed, in.TitleTemplate) userTpl = AppendAttributeConstraints(userTpl, allowed, in.TitleTemplate)
@@ -113,6 +113,8 @@ func TestResolveProductPromptTemplates_derivesFormulaFromPromptHTML(t *testing.T
aiprompts.SectionDescriptionStart + "\n" + aiprompts.SectionDescriptionStart + "\n" +
`<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>` + `<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>` +
`<p>{Napiši odstavek, ki je dolg 100 besed.}</p>` + `<p>{Napiši odstavek, ki je dolg 100 besed.}</p>` +
`<H2>{Izpostavi en benefit}</H2>` +
`<p>{Napiši odstavek z tipom izdelka lowercase.}</p>` +
`<b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij}</li></ul>` + "\n" + `<b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij}</li></ul>` + "\n" +
aiprompts.SectionDescriptionEnd aiprompts.SectionDescriptionEnd
sys, user := resolveProductPromptTemplates(ProductInput{ sys, user := resolveProductPromptTemplates(ProductInput{
@@ -133,9 +135,22 @@ func TestResolveProductPromptTemplates_derivesFormulaFromPromptHTML(t *testing.T
if !strings.Contains(sys, `"name" MUST be rewritten`) { if !strings.Contains(sys, `"name" MUST be rewritten`) {
t.Fatalf("missing title rewrite system override:\n%s", sys) t.Fatalf("missing title rewrite system override:\n%s", sys)
} }
if descriptionSatisfiesFormula("plain Slovenian prose without tags", aiprompts.EffectiveDescriptionTemplateAny(nil, prompt)) { if !strings.Contains(user, "Title instructions (REQUIRED") {
t.Fatalf("missing Title section constraint:\n%s", user)
}
effective := aiprompts.EffectiveDescriptionTemplateAny(nil, prompt)
if descriptionSatisfiesFormula("plain Slovenian prose without tags", effective) {
t.Fatal("plain prose must fail derived formula gate") t.Fatal("plain prose must fail derived formula gate")
} }
// One h2 is not enough when formula asks for two.
thin := "<h2>A</h2><p>One</p><p>Two</p><ul><li>x</li></ul>"
if descriptionSatisfiesFormula(thin, effective) {
t.Fatal("under-counted h2 must fail formula gate")
}
ok := "<h2>A</h2><p>One</p><h2>B</h2><p>Two</p><ul><li>x</li></ul>"
if !descriptionSatisfiesFormula(ok, effective) {
t.Fatal("full section count must pass formula gate")
}
} }
func TestResolveProductPromptTemplates_fallsBackToCompany(t *testing.T) { func TestResolveProductPromptTemplates_fallsBackToCompany(t *testing.T) {
+3
View File
@@ -278,6 +278,9 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
catPrompt = categoryEnhancePromptFor(in.CategoryPromptsByLang, out.Category, lang, primary) catPrompt = categoryEnhancePromptFor(in.CategoryPromptsByLang, out.Category, lang, primary)
} }
titleTpl, descTpl := categoryFormulasFor(in, out.Category) titleTpl, descTpl := categoryFormulasFor(in, out.Category)
if effective := aiprompts.EffectiveDescriptionTemplateAny(descTpl, catPrompt); effective != nil {
descTpl = effective
}
priorFields := company.FieldsForLanguage(in.PriorLocalized, lang) priorFields := company.FieldsForLanguage(in.PriorLocalized, lang)
priorHash := priorFields.EnhanceInputHash priorHash := priorFields.EnhanceInputHash
priorName := priorFields.ProcessedName priorName := priorFields.ProcessedName