fix
This commit is contained in:
@@ -161,29 +161,63 @@ 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
|
||||||
@@ -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" +
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user