fix
This commit is contained in:
@@ -56,7 +56,7 @@ type DefaultTemplate struct {
|
|||||||
// Used by local A1/Demo prompt repair and seed-a1 overlays.
|
// 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).
|
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
|
- name: short retail title; follow any Title formula constraints that follow; use Attrs
|
||||||
- description: prefer 1-3 factual paragraphs as ONE string; limited HTML (<h2><p><ul><li>) is allowed if needed — do NOT emit a competing full HTML document, <name>/<metaDescription> blocks, or separate schema
|
- 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
|
||||||
|
|
||||||
Category: {{category}}
|
Category: {{category}}
|
||||||
Name: {{name}}
|
Name: {{name}}
|
||||||
@@ -84,8 +84,8 @@ Rules:
|
|||||||
- Reply with ONLY JSON (no markdown)
|
- Reply with ONLY JSON (no markdown)
|
||||||
- Schema: {"name":"string","description":"string"}
|
- Schema: {"name":"string","description":"string"}
|
||||||
- name: short retail title
|
- name: short retail title
|
||||||
- description: 1-2 factual sentences; never copy name as description
|
- 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 1-2 factual sentences from Category and Attrs only (do not invent specs)
|
- 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}}
|
- Write name and description in {{language}}
|
||||||
Example:
|
Example:
|
||||||
{"name":"Acme Widget Pro","description":"Durable widget for everyday use. Clear specs, ready to ship."}
|
{"name":"Acme Widget Pro","description":"Durable widget for everyday use. Clear specs, ready to ship."}
|
||||||
|
|||||||
@@ -56,4 +56,10 @@ func TestBuiltInProductEnhanceUsesSharedUserTemplate(t *testing.T) {
|
|||||||
if def.UserTemplate != CategoryEnhanceUserTemplate {
|
if def.UserTemplate != CategoryEnhanceUserTemplate {
|
||||||
t.Fatalf("UserTemplate must be CategoryEnhanceUserTemplate")
|
t.Fatalf("UserTemplate must be CategoryEnhanceUserTemplate")
|
||||||
}
|
}
|
||||||
|
if strings.Contains(def.SystemTemplate, "1-2 factual sentences") {
|
||||||
|
t.Fatal("system template must not force 1-2 sentences over category Description formulas")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(def.SystemTemplate), "description formula") {
|
||||||
|
t.Fatal("system template should defer to Description formula when present")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func TestRunSteps_callsEnhanceWhenInputHashDiffers(t *testing.T) {
|
|||||||
e := &Engine{
|
e := &Engine{
|
||||||
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
|
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
|
||||||
calls++
|
calls++
|
||||||
return Completion{Text: `{"name":"Fresh","description":"New copy."}`, TotalTokens: 3}, nil
|
return Completion{Text: `{"name":"Fresh","description":"Fresh retail copy with enough detail for a quality enhance hash."}`, TotalTokens: 3}, nil
|
||||||
}},
|
}},
|
||||||
Vector: NoopVectorCategorizer{},
|
Vector: NoopVectorCategorizer{},
|
||||||
}
|
}
|
||||||
@@ -243,6 +243,42 @@ func TestRunSteps_thinOKDoesNotPersistEnhanceHash(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunSteps_synthesizedDescDoesNotPersistEnhanceHash(t *testing.T) {
|
||||||
|
e := &Engine{
|
||||||
|
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
|
||||||
|
// Empty description forces synthesizeProductDescription.
|
||||||
|
return Completion{Text: `{"name":"GIGABYTE GS27QC","description":""}`, TotalTokens: 3}, nil
|
||||||
|
}},
|
||||||
|
Vector: NoopVectorCategorizer{},
|
||||||
|
}
|
||||||
|
descTpl := map[string]any{
|
||||||
|
"sections": []any{
|
||||||
|
map[string]any{"type": "h1", "instructions": "Heading"},
|
||||||
|
map[string]any{"type": "p", "instructions": "Body"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
out, err := e.RunSteps(context.Background(), "co", ProductInput{
|
||||||
|
Name: "GIGABYTE GS27QC",
|
||||||
|
Mapped: map[string]any{"name": "GIGABYTE GS27QC", "category": "7", "brand": "GIGABYTE"},
|
||||||
|
DescriptionTemplate: descTpl,
|
||||||
|
Language: "sl",
|
||||||
|
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out.ProcessedDescription, "<h1>") {
|
||||||
|
t.Fatalf("expected formula synthesize HTML, got %q", out.ProcessedDescription)
|
||||||
|
}
|
||||||
|
if h, _ := out.FieldSources[FieldEnhanceInputHash].(string); h != "" {
|
||||||
|
t.Fatalf("synthesized description must not persist enhance hash (got %q)", h)
|
||||||
|
}
|
||||||
|
for _, lf := range out.LocalizedContent {
|
||||||
|
if lf.EnhanceInputHash != "" {
|
||||||
|
t.Fatalf("localized synthesized hash must be empty: %+v", lf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunSteps_weakPriorHashDoesNotSkipReprocess(t *testing.T) {
|
func TestRunSteps_weakPriorHashDoesNotSkipReprocess(t *testing.T) {
|
||||||
calls := 0
|
calls := 0
|
||||||
e := &Engine{
|
e := &Engine{
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ func FormatDescriptionFormulaConstraint(template any) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("Description formula (cover each section in order; write in {{language}}):\n")
|
b.WriteString("Description formula (REQUIRED — overrides any shorter \"1-2 sentences\" rule):\n")
|
||||||
|
b.WriteString("Emit description as ONE HTML string covering each section in order in {{language}}.\n")
|
||||||
|
b.WriteString("Use tags matching section type: h1/h2/h3/h4 → <hN>…</hN>, p → <p>…</p>, ul → <ul><li>…</li></ul>.\n")
|
||||||
for _, s := range sections {
|
for _, s := range sections {
|
||||||
typ := strings.TrimSpace(s.Type)
|
typ := strings.TrimSpace(s.Type)
|
||||||
instr := strings.TrimSpace(s.Instructions)
|
instr := strings.TrimSpace(s.Instructions)
|
||||||
@@ -95,6 +97,27 @@ func FormatDescriptionFormulaConstraint(template any) string {
|
|||||||
return strings.TrimSpace(b.String())
|
return strings.TrimSpace(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// descriptionFormulaSystemOverride is appended to the enhance system template when a
|
||||||
|
// category description_template is present so company/built-in "1-2 sentences" rules
|
||||||
|
// cannot override the per-category formula (A1 category definition pages).
|
||||||
|
const descriptionFormulaSystemOverride = `When the user message includes a "Description formula", obey those sections over any shorter "1-2 sentences" / "1-3 paragraphs" guidance: emit one HTML string using tags matching each section type (h1/h2/h3/h4, p, ul), in order, in {{language}}. Never ignore the Description formula.`
|
||||||
|
|
||||||
|
// AppendDescriptionFormulaSystemOverride strengthens the system prompt when a
|
||||||
|
// description formula is active. No-op when template is empty/unparseable.
|
||||||
|
func AppendDescriptionFormulaSystemOverride(systemTpl string, descriptionTemplate any) string {
|
||||||
|
if FormatDescriptionFormulaConstraint(descriptionTemplate) == "" {
|
||||||
|
return strings.TrimSpace(systemTpl)
|
||||||
|
}
|
||||||
|
systemTpl = strings.TrimSpace(systemTpl)
|
||||||
|
if systemTpl == "" {
|
||||||
|
return descriptionFormulaSystemOverride
|
||||||
|
}
|
||||||
|
if strings.Contains(systemTpl, "Description formula") {
|
||||||
|
return systemTpl
|
||||||
|
}
|
||||||
|
return systemTpl + "\n" + descriptionFormulaSystemOverride
|
||||||
|
}
|
||||||
|
|
||||||
type titleFormulaElement struct {
|
type titleFormulaElement struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ func TestFormatDescriptionFormulaConstraint(t *testing.T) {
|
|||||||
map[string]any{"type": "ul", "instructions": "3 bullets of specs"},
|
map[string]any{"type": "ul", "instructions": "3 bullets of specs"},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
if !strings.Contains(got, "REQUIRED") || !strings.Contains(got, "<hN>") {
|
||||||
|
t.Fatalf("missing required HTML guidance: %s", got)
|
||||||
|
}
|
||||||
if !strings.Contains(got, "- h1: Main product heading") {
|
if !strings.Contains(got, "- h1: Main product heading") {
|
||||||
t.Fatalf("missing h1: %s", got)
|
t.Fatalf("missing h1: %s", got)
|
||||||
}
|
}
|
||||||
@@ -49,6 +52,30 @@ func TestFormatDescriptionFormulaConstraint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAppendDescriptionFormulaSystemOverride(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
desc := map[string]any{
|
||||||
|
"sections": []any{
|
||||||
|
map[string]any{"type": "h1", "instructions": "Heading"},
|
||||||
|
map[string]any{"type": "p", "instructions": "Body"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sys, user := resolveProductPromptTemplates(ProductInput{
|
||||||
|
EnhanceSystemTemplate: "Retail product copywriter.\n- description: 1-2 factual sentences",
|
||||||
|
EnhanceUserTemplate: "Name: {{name}}",
|
||||||
|
DescriptionTemplate: desc,
|
||||||
|
})
|
||||||
|
if !strings.Contains(user, "Description formula") {
|
||||||
|
t.Fatalf("missing user formula: %s", user)
|
||||||
|
}
|
||||||
|
if !strings.Contains(sys, "Description formula") || !strings.Contains(sys, "1-2 sentences") {
|
||||||
|
t.Fatalf("system should keep company text and append formula override: %s", sys)
|
||||||
|
}
|
||||||
|
if AppendDescriptionFormulaSystemOverride("plain", nil) != "plain" {
|
||||||
|
t.Fatal("empty formula must be no-op")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAppendFormulaConstraints_injectedIntoResolve(t *testing.T) {
|
func TestAppendFormulaConstraints_injectedIntoResolve(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
title := map[string]any{
|
title := map[string]any{
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string)
|
|||||||
}
|
}
|
||||||
// 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, in.DescriptionTemplate)
|
userTpl = AppendFormulaConstraints(userTpl, in.TitleTemplate, in.DescriptionTemplate)
|
||||||
|
// Company/built-in system prompts often say "1-2 sentences"; when a category
|
||||||
|
// description formula exists, override that so process matches A1 category defs.
|
||||||
|
systemTpl = AppendDescriptionFormulaSystemOverride(systemTpl, in.DescriptionTemplate)
|
||||||
return systemTpl, userTpl
|
return systemTpl, userTpl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -326,19 +326,24 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
|||||||
// Prefer title-aware selection + synthesize before deciding hash persistence.
|
// Prefer title-aware selection + synthesize before deciding hash persistence.
|
||||||
name = preferredProductTitle(in.GTIN, name, out.Name, priorName, in.Name)
|
name = preferredProductTitle(in.GTIN, name, out.Name, priorName, in.Name)
|
||||||
desc = preferredProductDescription(name, desc, out.Description, priorDesc)
|
desc = preferredProductDescription(name, desc, out.Description, priorDesc)
|
||||||
|
outerSynth := false
|
||||||
if isWeakPriorEnhanceDescription(desc, name) {
|
if isWeakPriorEnhanceDescription(desc, name) {
|
||||||
if synth := synthesizeDescriptionFromTitle(name, displayCat, lang, enhanceAttrs); synth != "" {
|
if synth := synthesizeProductDescription(name, displayCat, lang, enhanceAttrs, descTpl); synth != "" {
|
||||||
desc = synth
|
desc = synth
|
||||||
|
outerSynth = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
weakDesc := isWeakPriorEnhanceDescription(desc, name)
|
weakDesc := isWeakPriorEnhanceDescription(desc, name)
|
||||||
// Only persist enhance_input_hash for quality ok / hash-skip unchanged.
|
// Only persist enhance_input_hash for quality ok / hash-skip unchanged.
|
||||||
// Never copy input_hash from error/passthrough/thin meta into localized.
|
// Never copy input_hash from error/passthrough/thin/synthesized meta.
|
||||||
persistHash := ""
|
persistHash := ""
|
||||||
rawHash := enhanceHashFromMeta(raw)
|
rawHash := enhanceHashFromMeta(raw)
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
persistHash = ""
|
persistHash = ""
|
||||||
|
case status == "synthesized" || outerSynth:
|
||||||
|
persistHash = ""
|
||||||
|
allUnchanged = false
|
||||||
case status == "unchanged" && !weakDesc:
|
case status == "unchanged" && !weakDesc:
|
||||||
persistHash = rawHash
|
persistHash = rawHash
|
||||||
case status == "ok" && !weakDesc:
|
case status == "ok" && !weakDesc:
|
||||||
@@ -402,7 +407,8 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
|||||||
// Timeout/error/empty: always synthesize a factual fallback when a title exists.
|
// Timeout/error/empty: always synthesize a factual fallback when a title exists.
|
||||||
if out.ProcessedName != "" && (out.ProcessedDescription == "" ||
|
if out.ProcessedName != "" && (out.ProcessedDescription == "" ||
|
||||||
isWeakPriorEnhanceDescription(out.ProcessedDescription, out.ProcessedName, out.Name)) {
|
isWeakPriorEnhanceDescription(out.ProcessedDescription, out.ProcessedName, out.Name)) {
|
||||||
if synth := synthesizeDescriptionFromTitle(out.ProcessedName, displayCat, primary, enhanceAttrs); synth != "" {
|
_, failDescTpl := categoryFormulasFor(in, out.Category)
|
||||||
|
if synth := synthesizeProductDescription(out.ProcessedName, displayCat, primary, enhanceAttrs, failDescTpl); synth != "" {
|
||||||
out.ProcessedDescription = synth
|
out.ProcessedDescription = synth
|
||||||
if out.Description == "" || isWeakPriorEnhanceDescription(out.Description, out.Name, out.ProcessedName) {
|
if out.Description == "" || isWeakPriorEnhanceDescription(out.Description, out.Name, out.ProcessedName) {
|
||||||
out.Description = synth
|
out.Description = synth
|
||||||
@@ -477,15 +483,16 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
|||||||
if out.ProcessedName == "" {
|
if out.ProcessedName == "" {
|
||||||
out.ProcessedName = out.Name
|
out.ProcessedName = out.Name
|
||||||
}
|
}
|
||||||
|
_, finalDescTpl := categoryFormulasFor(in, out.Category)
|
||||||
if out.ProcessedDescription == "" || isWeakPriorEnhanceDescription(out.ProcessedDescription, out.ProcessedName, out.Name) {
|
if out.ProcessedDescription == "" || isWeakPriorEnhanceDescription(out.ProcessedDescription, out.ProcessedName, out.Name) {
|
||||||
if synth := synthesizeDescriptionFromTitle(out.ProcessedName, displayCat, in.Language, out.Attributes); synth != "" {
|
if synth := synthesizeProductDescription(out.ProcessedName, displayCat, in.Language, out.Attributes, finalDescTpl); synth != "" {
|
||||||
out.ProcessedDescription = synth
|
out.ProcessedDescription = synth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if out.Description == "" || isWeakPriorEnhanceDescription(out.Description, out.Name, out.ProcessedName) {
|
if out.Description == "" || isWeakPriorEnhanceDescription(out.Description, out.Name, out.ProcessedName) {
|
||||||
if out.ProcessedDescription != "" && !isWeakPriorEnhanceDescription(out.ProcessedDescription, out.Name, out.ProcessedName) {
|
if out.ProcessedDescription != "" && !isWeakPriorEnhanceDescription(out.ProcessedDescription, out.Name, out.ProcessedName) {
|
||||||
out.Description = out.ProcessedDescription
|
out.Description = out.ProcessedDescription
|
||||||
} else if synth := synthesizeDescriptionFromTitle(out.Name, displayCat, in.Language, out.Attributes); synth != "" {
|
} else if synth := synthesizeProductDescription(out.Name, displayCat, in.Language, out.Attributes, finalDescTpl); synth != "" {
|
||||||
out.Description = synth
|
out.Description = synth
|
||||||
if out.ProcessedDescription == "" || isWeakPriorEnhanceDescription(out.ProcessedDescription, out.ProcessedName, out.Name) {
|
if out.ProcessedDescription == "" || isWeakPriorEnhanceDescription(out.ProcessedDescription, out.ProcessedName, out.Name) {
|
||||||
out.ProcessedDescription = synth
|
out.ProcessedDescription = synth
|
||||||
@@ -657,7 +664,7 @@ func (e *Engine) enhance(ctx context.Context, in ProductInput, category string,
|
|||||||
name := preferredProductTitle(in.GTIN, in.Name, in.PriorProcessedName)
|
name := preferredProductTitle(in.GTIN, in.Name, in.PriorProcessedName)
|
||||||
desc := preferredProductDescription(name, in.Description, in.PriorProcessedDescription)
|
desc := preferredProductDescription(name, in.Description, in.PriorProcessedDescription)
|
||||||
if isWeakPriorEnhanceDescription(desc, name) {
|
if isWeakPriorEnhanceDescription(desc, name) {
|
||||||
if synth := synthesizeDescriptionFromTitle(name, category, in.Language, attrs); synth != "" {
|
if synth := synthesizeProductDescription(name, category, in.Language, attrs, in.DescriptionTemplate); synth != "" {
|
||||||
desc = synth
|
desc = synth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -676,18 +683,22 @@ func (e *Engine) enhance(ctx context.Context, in ProductInput, category string,
|
|||||||
}
|
}
|
||||||
name := preferredProductTitle(in.GTIN, SanitizeOutput(fmt.Sprint(obj["name"])), in.Name, in.PriorProcessedName)
|
name := preferredProductTitle(in.GTIN, SanitizeOutput(fmt.Sprint(obj["name"])), in.Name, in.PriorProcessedName)
|
||||||
desc := preferredProductDescription(name, SanitizeOutput(fmt.Sprint(obj["description"])), in.Description, in.PriorProcessedDescription)
|
desc := preferredProductDescription(name, SanitizeOutput(fmt.Sprint(obj["description"])), in.Description, in.PriorProcessedDescription)
|
||||||
|
synthesized := false
|
||||||
if isWeakPriorEnhanceDescription(desc, name) {
|
if isWeakPriorEnhanceDescription(desc, name) {
|
||||||
if synth := synthesizeDescriptionFromTitle(name, category, in.Language, attrs); synth != "" {
|
if synth := synthesizeProductDescription(name, category, in.Language, attrs, in.DescriptionTemplate); synth != "" {
|
||||||
desc = synth
|
desc = synth
|
||||||
|
synthesized = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
meta := map[string]any{
|
meta := map[string]any{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"raw": comp.Raw,
|
"raw": comp.Raw,
|
||||||
}
|
}
|
||||||
// Only attach input_hash when output description is quality-worthy; thin
|
// Heuristic synthesize must not poison enhance_input_hash (would hash-skip
|
||||||
// title-echo / heuristic filler must not poison field_sources / localized skip hashes.
|
// formula-aware LLM copy on reprocess). Only persist hash for real LLM quality.
|
||||||
if !isWeakPriorEnhanceDescription(desc, name) {
|
if synthesized {
|
||||||
|
meta["status"] = "synthesized"
|
||||||
|
} else if !isWeakPriorEnhanceDescription(desc, name) {
|
||||||
meta["input_hash"] = hash
|
meta["input_hash"] = hash
|
||||||
}
|
}
|
||||||
return name, desc, comp.TotalTokens, meta, nil
|
return name, desc, comp.TotalTokens, meta, nil
|
||||||
@@ -948,6 +959,73 @@ func formatAttrDimParts(attrs map[string]any, maxParts int) []string {
|
|||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// synthesizeProductDescription prefers a category description_template skeleton
|
||||||
|
// (HTML section types) when present; otherwise falls back to plain title synthesize.
|
||||||
|
func synthesizeProductDescription(title, category, language string, attrs map[string]any, descriptionTemplate any) string {
|
||||||
|
if sections, ok := parseDescriptionFormulaSections(descriptionTemplate); ok && len(sections) > 0 {
|
||||||
|
if out := synthesizeDescriptionFromFormula(title, category, language, attrs, sections); out != "" {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return synthesizeDescriptionFromTitle(title, category, language, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// synthesizeDescriptionFromFormula builds a minimal HTML description matching
|
||||||
|
// category description_template section types so timeout/fallback still respects
|
||||||
|
// A1 category structure (unlike plain title synthesize).
|
||||||
|
func synthesizeDescriptionFromFormula(title, category, language string, attrs map[string]any, sections []descriptionFormulaSection) string {
|
||||||
|
title = strings.TrimSpace(title)
|
||||||
|
if title == "" || title == "<nil>" || isPromptLabelTitle(title) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
base := synthesizeDescriptionFromTitle(title, category, language, attrs)
|
||||||
|
if base == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
dims := formatAttrDimParts(attrs, 6)
|
||||||
|
var b strings.Builder
|
||||||
|
paraUsed := false
|
||||||
|
listUsed := false
|
||||||
|
for _, s := range sections {
|
||||||
|
typ := strings.ToLower(strings.TrimSpace(s.Type))
|
||||||
|
switch typ {
|
||||||
|
case "h1", "h2", "h3", "h4":
|
||||||
|
heading := title
|
||||||
|
if typ != "h1" {
|
||||||
|
if isSlovenianContentLanguage(language) {
|
||||||
|
heading = "Ključne lastnosti"
|
||||||
|
} else {
|
||||||
|
heading = "Key features"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "<%s>%s</%s>", typ, SanitizeOutput(heading), typ)
|
||||||
|
case "ul":
|
||||||
|
b.WriteString("<ul>")
|
||||||
|
items := dims
|
||||||
|
if len(items) == 0 {
|
||||||
|
items = []string{base}
|
||||||
|
}
|
||||||
|
for _, it := range items {
|
||||||
|
fmt.Fprintf(&b, "<li>%s</li>", SanitizeOutput(it))
|
||||||
|
}
|
||||||
|
b.WriteString("</ul>")
|
||||||
|
listUsed = true
|
||||||
|
default: // p and unknown → paragraph
|
||||||
|
body := base
|
||||||
|
if paraUsed && len(dims) > 0 && !listUsed {
|
||||||
|
if isSlovenianContentLanguage(language) {
|
||||||
|
body = "Ključne specifikacije: " + strings.Join(dims, ", ") + "."
|
||||||
|
} else {
|
||||||
|
body = "Key specs: " + strings.Join(dims, ", ") + "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "<p>%s</p>", SanitizeOutput(body))
|
||||||
|
paraUsed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SanitizeOutput(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
// synthesizeDescriptionFromTitle builds a short factual fallback when enhance
|
// synthesizeDescriptionFromTitle builds a short factual fallback when enhance
|
||||||
// returns empty/title-echo/filler copy. Uses title, category, brand, model, and
|
// returns empty/title-echo/filler copy. Uses title, category, brand, model, and
|
||||||
// key dims. language is a content-language code (en/sl/…) or English label.
|
// key dims. language is a content-language code (en/sl/…) or English label.
|
||||||
|
|||||||
@@ -46,6 +46,30 @@ func TestIsWeakPriorEnhanceDescription_fillerPhrases(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSynthesizeProductDescription_formulaHTML(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
title := "GIGABYTE GS27QC"
|
||||||
|
attrs := map[string]any{"brand": "GIGABYTE", "width": "61 cm"}
|
||||||
|
tpl := map[string]any{
|
||||||
|
"sections": []any{
|
||||||
|
map[string]any{"type": "h1", "instructions": "Main heading"},
|
||||||
|
map[string]any{"type": "p", "instructions": "Intro"},
|
||||||
|
map[string]any{"type": "ul", "instructions": "Specs"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
got := synthesizeProductDescription(title, "Gaming monitorji", "sl", attrs, tpl)
|
||||||
|
if !strings.Contains(got, "<h1>") || !strings.Contains(got, "<p>") || !strings.Contains(got, "<ul>") {
|
||||||
|
t.Fatalf("expected HTML skeleton from formula, got %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, title) {
|
||||||
|
t.Fatalf("expected title in formula synth: %q", got)
|
||||||
|
}
|
||||||
|
plain := synthesizeProductDescription(title, "Gaming monitorji", "sl", attrs, nil)
|
||||||
|
if strings.Contains(plain, "<h1>") {
|
||||||
|
t.Fatalf("nil formula should stay plain: %q", plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSynthesizeDescriptionFromTitle_brandModelCategory(t *testing.T) {
|
func TestSynthesizeDescriptionFromTitle_brandModelCategory(t *testing.T) {
|
||||||
title := "Vogel's WALL 3245 TV Wall Mount"
|
title := "Vogel's WALL 3245 TV Wall Mount"
|
||||||
attrs := map[string]any{
|
attrs := map[string]any{
|
||||||
|
|||||||
Reference in New Issue
Block a user