fix
This commit is contained in:
@@ -160,3 +160,76 @@ func parseDescriptionFormula(template any) (DescriptionFormula, bool) {
|
|||||||
return f, true
|
return f, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EffectiveDescriptionFormula returns the stored description_template when it has
|
||||||
|
// sections; otherwise derives sections from the category enhance prompt Description
|
||||||
|
// role (legacy HTML <H2>{…}</H2><p>{…}</p> overlays). That is how A1 and tenants
|
||||||
|
// author HTML structure in /categories/.../prompt — without this, enhance accepts
|
||||||
|
// plain prose and never runs the formula quality gate.
|
||||||
|
func EffectiveDescriptionFormula(stored any, categoryPrompt string) DescriptionFormula {
|
||||||
|
if f, ok := parseDescriptionFormula(stored); ok && len(f.Sections) > 0 {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
body := ExtractEnhanceSectionBody(categoryPrompt, SectionDescriptionStart, SectionDescriptionEnd)
|
||||||
|
if body == "" {
|
||||||
|
body = strings.TrimSpace(categoryPrompt)
|
||||||
|
}
|
||||||
|
if body == "" || !reLegacyHTMLBlock.MatchString(body) {
|
||||||
|
if f, ok := parseDescriptionFormula(stored); ok {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
return DescriptionFormula{}
|
||||||
|
}
|
||||||
|
return DeriveDescriptionFormulaFromLegacyParts(LegacyEnhanceParts{
|
||||||
|
DescriptionRules: body,
|
||||||
|
WasLegacy: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// EffectiveDescriptionTemplateAny is EffectiveDescriptionFormula as a JSON object
|
||||||
|
// for processing.ProductInput.DescriptionTemplate (nil when empty).
|
||||||
|
func EffectiveDescriptionTemplateAny(stored any, categoryPrompt string) any {
|
||||||
|
f := EffectiveDescriptionFormula(stored, categoryPrompt)
|
||||||
|
if len(f.Sections) == 0 && strings.TrimSpace(f.MetaTitle) == "" && strings.TrimSpace(f.MetaDescription) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var out map[string]any
|
||||||
|
if err := json.Unmarshal(b, &out); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryTitlePromptRequiresRewrite reports whether the Title role instructs a
|
||||||
|
// new retail name (not keep/echo supplier title). Used to reject "Matched" copy-paste.
|
||||||
|
func CategoryTitlePromptRequiresRewrite(categoryPrompt string) bool {
|
||||||
|
body := ExtractEnhanceSectionBody(categoryPrompt, SectionTitleStart, SectionTitleEnd)
|
||||||
|
if body == "" {
|
||||||
|
body = strings.TrimSpace(categoryPrompt)
|
||||||
|
}
|
||||||
|
if body == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(body)
|
||||||
|
cues := []string{
|
||||||
|
"napiši novo ime",
|
||||||
|
"napisi novo ime",
|
||||||
|
"novo ime izdelka",
|
||||||
|
"po formuli",
|
||||||
|
"write a new",
|
||||||
|
"new product name",
|
||||||
|
"rename",
|
||||||
|
"title formula",
|
||||||
|
"sentence case",
|
||||||
|
}
|
||||||
|
for _, c := range cues {
|
||||||
|
if strings.Contains(lower, c) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,3 +54,53 @@ GPT predloga:
|
|||||||
t.Fatalf("derived template should be OK: %s", raw)
|
t.Fatalf("derived template should be OK: %s", raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEffectiveDescriptionFormula_fromPromptDescriptionSection(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
prompt := strings.Join([]string{
|
||||||
|
SectionTitleStart,
|
||||||
|
`Napiši novo ime izdelka po formuli: "tip izdelka sentence case", "znamka". Ne uporabljaj vejic.`,
|
||||||
|
SectionTitleEnd,
|
||||||
|
"",
|
||||||
|
SectionDescriptionStart,
|
||||||
|
`<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>`,
|
||||||
|
`<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>`,
|
||||||
|
`<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>`,
|
||||||
|
`<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi tip izdelka lowercase}</p>`,
|
||||||
|
`<b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>`,
|
||||||
|
SectionDescriptionEnd,
|
||||||
|
}, "\n")
|
||||||
|
f := EffectiveDescriptionFormula(nil, prompt)
|
||||||
|
if len(f.Sections) < 4 {
|
||||||
|
t.Fatalf("want HTML sections from Description role, got %#v", f.Sections)
|
||||||
|
}
|
||||||
|
if f.Sections[0].Type != "h2" || f.Sections[1].Type != "p" {
|
||||||
|
t.Fatalf("unexpected order: %#v", f.Sections)
|
||||||
|
}
|
||||||
|
last := f.Sections[len(f.Sections)-1]
|
||||||
|
if last.Type != "ul" {
|
||||||
|
t.Fatalf("last should be ul specs, got %#v", last)
|
||||||
|
}
|
||||||
|
anyTpl := EffectiveDescriptionTemplateAny(nil, prompt)
|
||||||
|
if anyTpl == nil {
|
||||||
|
t.Fatal("expected non-nil template any")
|
||||||
|
}
|
||||||
|
if !CategoryTitlePromptRequiresRewrite(prompt) {
|
||||||
|
t.Fatal("Title section should require rewrite")
|
||||||
|
}
|
||||||
|
if CategoryTitlePromptRequiresRewrite("--- Title ---\nKeep supplier name.\n--- End Title ---") {
|
||||||
|
t.Fatal("keep-only Title must not require rewrite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractEnhanceSectionBody(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
prompt := SectionTitleStart + "\nHello\n" + SectionTitleEnd + "\n" +
|
||||||
|
SectionDescriptionStart + "\nBody here\n" + SectionDescriptionEnd
|
||||||
|
if got := ExtractEnhanceSectionBody(prompt, SectionTitleStart, SectionTitleEnd); got != "Hello" {
|
||||||
|
t.Fatalf("title body=%q", got)
|
||||||
|
}
|
||||||
|
if got := ExtractEnhanceSectionBody(prompt, SectionDescriptionStart, SectionDescriptionEnd); got != "Body here" {
|
||||||
|
t.Fatalf("desc body=%q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -110,6 +110,31 @@ func BuildCategoryEnhanceOverlay(parts LegacyEnhanceParts) string {
|
|||||||
return strings.TrimSpace(b.String())
|
return strings.TrimSpace(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractEnhanceSectionBody returns the text between start/end markers
|
||||||
|
// (case-insensitive markers, body preserved). Empty when the start marker is missing.
|
||||||
|
func ExtractEnhanceSectionBody(prompt, start, end string) string {
|
||||||
|
raw := strings.ReplaceAll(strings.ReplaceAll(prompt, "\r\n", "\n"), "\r", "\n")
|
||||||
|
lower := strings.ToLower(raw)
|
||||||
|
startL := strings.ToLower(strings.TrimSpace(start))
|
||||||
|
endL := strings.ToLower(strings.TrimSpace(end))
|
||||||
|
if startL == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
startIdx := strings.Index(lower, startL)
|
||||||
|
if startIdx < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
bodyStart := startIdx + len(startL)
|
||||||
|
rest := raw[bodyStart:]
|
||||||
|
restLower := lower[bodyStart:]
|
||||||
|
if endL != "" {
|
||||||
|
if endIdx := strings.Index(restLower, endL); endIdx >= 0 {
|
||||||
|
rest = rest[:endIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(strings.TrimPrefix(strings.TrimSuffix(rest, "\n"), "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
func cleanLegacyInstruction(s string) string {
|
func cleanLegacyInstruction(s string) string {
|
||||||
s = modernizeLegacyPlaceholders(s)
|
s = modernizeLegacyPlaceholders(s)
|
||||||
s = reDoubledQuotes.ReplaceAllString(s, `"`)
|
s = reDoubledQuotes.ReplaceAllString(s, `"`)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
||||||
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -276,6 +277,26 @@ func AppendTitleFormulaSystemOverride(systemTpl string, titleTemplate any) strin
|
|||||||
return systemTpl + "\n" + titleFormulaSystemOverride
|
return systemTpl + "\n" + titleFormulaSystemOverride
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// titlePromptRewriteOverride covers free-form Title section instructions
|
||||||
|
// (categories.prompt --- Title ---) that are not structured title_template.
|
||||||
|
const titlePromptRewriteOverride = `When the category Title section asks for a new product name or naming formula, "name" MUST be rewritten — never copy the supplier Name unchanged when type/brand/model/color (or other Title cues) are available. Obey the Title section over any "keep short retail title" guidance.`
|
||||||
|
|
||||||
|
// AppendTitlePromptRewriteOverride strengthens the system prompt when the
|
||||||
|
// category Title role requires a rewrite. No-op when Title does not ask for one.
|
||||||
|
func AppendTitlePromptRewriteOverride(systemTpl, categoryPrompt string) string {
|
||||||
|
if !aiprompts.CategoryTitlePromptRequiresRewrite(categoryPrompt) {
|
||||||
|
return strings.TrimSpace(systemTpl)
|
||||||
|
}
|
||||||
|
systemTpl = strings.TrimSpace(systemTpl)
|
||||||
|
if systemTpl == "" {
|
||||||
|
return titlePromptRewriteOverride
|
||||||
|
}
|
||||||
|
if strings.Contains(systemTpl, `"name" MUST be rewritten`) {
|
||||||
|
return systemTpl
|
||||||
|
}
|
||||||
|
return systemTpl + "\n" + titlePromptRewriteOverride
|
||||||
|
}
|
||||||
|
|
||||||
// categoryEnhanceSystemOverlay is appended when categories.prompt (enhance overlay)
|
// categoryEnhanceSystemOverlay is appended when categories.prompt (enhance overlay)
|
||||||
// is active so free-form category copy drives title, description, AND attributes —
|
// is active so free-form category copy drives title, description, AND attributes —
|
||||||
// not description alone — while Title/Description formulas keep structural precedence.
|
// not description alone — while Title/Description formulas keep structural precedence.
|
||||||
|
|||||||
@@ -24,18 +24,23 @@ func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string)
|
|||||||
userTpl = def.UserTemplate
|
userTpl = def.UserTemplate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Prefer stored description_template; else derive HTML sections from the
|
||||||
|
// category prompt Description role (A1-style <H2>{…}</H2> overlays).
|
||||||
|
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, in.DescriptionTemplate, in.OmitSEOMeta)
|
userTpl = AppendFormulaConstraints(userTpl, in.TitleTemplate, descTpl, in.OmitSEOMeta)
|
||||||
// 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)
|
||||||
// Company/built-in system prompts often say "1-2 sentences"; when a category
|
// Company/built-in system prompts often say "1-2 sentences"; when a category
|
||||||
// description formula exists, override that so process matches A1 category defs.
|
// description formula exists, override that so process matches A1 category defs.
|
||||||
systemTpl = AppendDescriptionFormulaSystemOverride(systemTpl, in.DescriptionTemplate)
|
systemTpl = AppendDescriptionFormulaSystemOverride(systemTpl, descTpl)
|
||||||
// Same for title_template vs "short retail title" — formulas win for name structure.
|
// Same for title_template vs "short retail title" — formulas win for name structure.
|
||||||
systemTpl = AppendTitleFormulaSystemOverride(systemTpl, in.TitleTemplate)
|
systemTpl = AppendTitleFormulaSystemOverride(systemTpl, in.TitleTemplate)
|
||||||
|
// Free-form Title section (prompt UI) that asks for a rewrite — not title_template.
|
||||||
|
systemTpl = AppendTitlePromptRewriteOverride(systemTpl, catPrompt)
|
||||||
if !in.OmitSEOMeta {
|
if !in.OmitSEOMeta {
|
||||||
systemTpl = AppendMetaFormulaSystemOverride(systemTpl, in.DescriptionTemplate)
|
systemTpl = AppendMetaFormulaSystemOverride(systemTpl, descTpl)
|
||||||
}
|
}
|
||||||
// categories.prompt applies to name, description, and attrs (not description-only).
|
// categories.prompt applies to name, description, and attrs (not description-only).
|
||||||
systemTpl = AppendCategoryEnhanceSystemOverlay(systemTpl, catPrompt)
|
systemTpl = AppendCategoryEnhanceSystemOverlay(systemTpl, catPrompt)
|
||||||
|
|||||||
@@ -105,6 +105,39 @@ func TestResolveProductPromptTemplates_categoryWithTitleFormula(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveProductPromptTemplates_derivesFormulaFromPromptHTML(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
prompt := aiprompts.SectionTitleStart + "\n" +
|
||||||
|
`Napiši novo ime izdelka po formuli: "tip izdelka sentence case", "znamka", "model". Ne uporabljaj vejic.` + "\n" +
|
||||||
|
aiprompts.SectionTitleEnd + "\n\n" +
|
||||||
|
aiprompts.SectionDescriptionStart + "\n" +
|
||||||
|
`<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>` +
|
||||||
|
`<p>{Napiši odstavek, ki je dolg 100 besed.}</p>` +
|
||||||
|
`<b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij}</li></ul>` + "\n" +
|
||||||
|
aiprompts.SectionDescriptionEnd
|
||||||
|
sys, user := resolveProductPromptTemplates(ProductInput{
|
||||||
|
EnhanceSystemTemplate: "Retail copywriter.\n- description: 1-2 sentences",
|
||||||
|
CategoryEnhancePrompt: prompt,
|
||||||
|
// Empty structured template — production A1 path often only has prompt HTML.
|
||||||
|
DescriptionTemplate: nil,
|
||||||
|
})
|
||||||
|
if !strings.Contains(user, "Description formula") {
|
||||||
|
t.Fatalf("expected Description formula derived from prompt HTML:\n%s", user)
|
||||||
|
}
|
||||||
|
if !strings.Contains(user, "- h2:") || !strings.Contains(user, "- p:") || !strings.Contains(user, "- ul:") {
|
||||||
|
t.Fatalf("expected h2/p/ul sections in formula:\n%s", user)
|
||||||
|
}
|
||||||
|
if !strings.Contains(sys, "Description formula") {
|
||||||
|
t.Fatalf("missing description system override:\n%s", sys)
|
||||||
|
}
|
||||||
|
if !strings.Contains(sys, `"name" MUST be rewritten`) {
|
||||||
|
t.Fatalf("missing title rewrite system override:\n%s", sys)
|
||||||
|
}
|
||||||
|
if descriptionSatisfiesFormula("plain Slovenian prose without tags", aiprompts.EffectiveDescriptionTemplateAny(nil, prompt)) {
|
||||||
|
t.Fatal("plain prose must fail derived formula gate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveProductPromptTemplates_fallsBackToCompany(t *testing.T) {
|
func TestResolveProductPromptTemplates_fallsBackToCompany(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
_, user := resolveProductPromptTemplates(ProductInput{
|
_, user := resolveProductPromptTemplates(ProductInput{
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
||||||
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
||||||
"github.com/descrybe/descrybe-v2/apps/api/internal/eprel"
|
"github.com/descrybe/descrybe-v2/apps/api/internal/eprel"
|
||||||
)
|
)
|
||||||
@@ -739,6 +741,10 @@ func appendStepLog(gpt map[string]any, name string, raw any) {
|
|||||||
// category description_template (short prose / title echo instead of multi-section HTML).
|
// category description_template (short prose / title echo instead of multi-section HTML).
|
||||||
const descriptionFormulaRetrySuffix = "\n\nINVALID DESCRIPTION. Your JSON ignored the Description formula. Reply with ONLY one JSON object; \"description\" must be ONE HTML string covering each formula section in order with matching tags (h1/h2/h3/h4, p, ul)."
|
const descriptionFormulaRetrySuffix = "\n\nINVALID DESCRIPTION. Your JSON ignored the Description formula. Reply with ONLY one JSON object; \"description\" must be ONE HTML string covering each formula section in order with matching tags (h1/h2/h3/h4, p, ul)."
|
||||||
|
|
||||||
|
// titleRewriteRetrySuffix is appended when the Title role requires a new retail
|
||||||
|
// name but the model echoed the supplier Name unchanged.
|
||||||
|
const titleRewriteRetrySuffix = "\n\nINVALID NAME. Category Title instructions require a NEW product name (formula / rewrite). \"name\" must NOT equal the supplier Name. Reply with ONLY one JSON object; rebuild \"name\" from type + brand + model + color (or the Title formula) in {{language}}."
|
||||||
|
|
||||||
// descriptionNeedsEnhanceRepair is true when desc is weak/empty/title-echo or
|
// descriptionNeedsEnhanceRepair is true when desc is weak/empty/title-echo or
|
||||||
// fails an active category description_template (A1 multi-section HTML).
|
// fails an active category description_template (A1 multi-section HTML).
|
||||||
// Heuristic invent/synth is intentionally excluded here — enhanceHashForceReason
|
// Heuristic invent/synth is intentionally excluded here — enhanceHashForceReason
|
||||||
@@ -750,6 +756,47 @@ func descriptionNeedsEnhanceRepair(desc string, template any, titles ...string)
|
|||||||
return !descriptionSatisfiesFormula(desc, template)
|
return !descriptionSatisfiesFormula(desc, template)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// titlesAreEquivalent treats supplier vs enriched names as the same when only
|
||||||
|
// case/spacing/punctuation differ — used to detect Title formula copy-paste.
|
||||||
|
func titlesAreEquivalent(a, b string) bool {
|
||||||
|
na := normalizeTitleCompare(a)
|
||||||
|
nb := normalizeTitleCompare(b)
|
||||||
|
if na == "" || nb == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return na == nb
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeTitleCompare(s string) string {
|
||||||
|
s = strings.TrimSpace(strings.ToLower(s))
|
||||||
|
if s == "" || s == "<nil>" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(s))
|
||||||
|
prevSpace := false
|
||||||
|
for _, r := range s {
|
||||||
|
switch {
|
||||||
|
case unicode.IsLetter(r) || unicode.IsDigit(r):
|
||||||
|
b.WriteRune(r)
|
||||||
|
prevSpace = false
|
||||||
|
case unicode.IsSpace(r) || r == '-' || r == '_' || r == '/' || r == ',':
|
||||||
|
if !prevSpace && b.Len() > 0 {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
prevSpace = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func titleNeedsRewriteRetry(in ProductInput, name string) bool {
|
||||||
|
if !aiprompts.CategoryTitlePromptRequiresRewrite(in.CategoryEnhancePrompt) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return titlesAreEquivalent(name, in.Name)
|
||||||
|
}
|
||||||
|
|
||||||
// enhanceHashForceReason returns why a matching prior enhance_input_hash must not
|
// enhanceHashForceReason returns why a matching prior enhance_input_hash must not
|
||||||
// skip the LLM (empty = safe to reuse as ai_enhance_unchanged).
|
// skip the LLM (empty = safe to reuse as ai_enhance_unchanged).
|
||||||
func enhanceHashForceReason(in ProductInput) string {
|
func enhanceHashForceReason(in ProductInput) string {
|
||||||
@@ -762,10 +809,18 @@ func enhanceHashForceReason(in ProductInput) string {
|
|||||||
if !descriptionSatisfiesFormula(in.PriorProcessedDescription, in.DescriptionTemplate) {
|
if !descriptionSatisfiesFormula(in.PriorProcessedDescription, in.DescriptionTemplate) {
|
||||||
return "formula-mismatch"
|
return "formula-mismatch"
|
||||||
}
|
}
|
||||||
|
if titleNeedsRewriteRetry(in, in.PriorProcessedName) {
|
||||||
|
return "title-copy-paste"
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) enhance(ctx context.Context, in ProductInput, category string, attrs map[string]any) (string, string, int, any, error) {
|
func (e *Engine) enhance(ctx context.Context, in ProductInput, category string, attrs map[string]any) (string, string, int, any, error) {
|
||||||
|
// Prompt Description HTML overlays count as the formula when description_template
|
||||||
|
// is empty — otherwise enhance accepts supplier-like prose and skips formula retry.
|
||||||
|
if effective := aiprompts.EffectiveDescriptionTemplateAny(in.DescriptionTemplate, in.CategoryEnhancePrompt); effective != nil {
|
||||||
|
in.DescriptionTemplate = effective
|
||||||
|
}
|
||||||
catUID := strings.TrimSpace(in.CategoryUniqueID)
|
catUID := strings.TrimSpace(in.CategoryUniqueID)
|
||||||
catName := strings.TrimSpace(category)
|
catName := strings.TrimSpace(category)
|
||||||
sysTpl, userTpl := resolveProductPromptTemplates(in)
|
sysTpl, userTpl := resolveProductPromptTemplates(in)
|
||||||
@@ -885,11 +940,19 @@ func (e *Engine) enhance(ctx context.Context, in ProductInput, category string,
|
|||||||
// Quality-gate on LLM description alone (do not absorb originals yet).
|
// Quality-gate on LLM description alone (do not absorb originals yet).
|
||||||
desc := preferredProductDescription(name, llmDesc)
|
desc := preferredProductDescription(name, llmDesc)
|
||||||
didFormulaRetry := false
|
didFormulaRetry := false
|
||||||
// Fast garbage that ignores A1 description_template: one formula-aware retry, then synthesize.
|
needsDescRetry := !descriptionSatisfiesFormula(desc, in.DescriptionTemplate) &&
|
||||||
if !descriptionSatisfiesFormula(desc, in.DescriptionTemplate) &&
|
FormatDescriptionFormulaConstraint(in.DescriptionTemplate) != ""
|
||||||
FormatDescriptionFormulaConstraint(in.DescriptionTemplate) != "" {
|
needsTitleRetry := titleNeedsRewriteRetry(in, name)
|
||||||
|
// One retry when Description HTML formula and/or Title rewrite were ignored.
|
||||||
|
if needsDescRetry || needsTitleRetry {
|
||||||
didFormulaRetry = true
|
didFormulaRetry = true
|
||||||
retryUser := user + descriptionFormulaRetrySuffix
|
retryUser := user
|
||||||
|
if needsDescRetry {
|
||||||
|
retryUser += descriptionFormulaRetrySuffix
|
||||||
|
}
|
||||||
|
if needsTitleRetry {
|
||||||
|
retryUser += titleRewriteRetrySuffix
|
||||||
|
}
|
||||||
retryStarted := time.Now()
|
retryStarted := time.Now()
|
||||||
comp2, obj2, err2 := CompleteJSON(ctx, e.Completer, system, retryUser, CompleteOptions{
|
comp2, obj2, err2 := CompleteJSON(ctx, e.Completer, system, retryUser, CompleteOptions{
|
||||||
MaxTokens: MaxTokensEnhance,
|
MaxTokens: MaxTokensEnhance,
|
||||||
@@ -904,7 +967,12 @@ func (e *Engine) enhance(ctx context.Context, in ProductInput, category string,
|
|||||||
n2 := SanitizeOutput(fmt.Sprint(obj2["name"]))
|
n2 := SanitizeOutput(fmt.Sprint(obj2["name"]))
|
||||||
d2 := SanitizeOutput(fmt.Sprint(obj2["description"]))
|
d2 := SanitizeOutput(fmt.Sprint(obj2["description"]))
|
||||||
if llmEnhanceHardRefuseReason(n2, d2) == "" {
|
if llmEnhanceHardRefuseReason(n2, d2) == "" {
|
||||||
name = preferredProductTitle(in.GTIN, n2, name, in.Name, in.PriorProcessedName)
|
// Prefer rewritten title over supplier echo when Title role requires it.
|
||||||
|
if needsTitleRetry && n2 != "" && !titlesAreEquivalent(n2, in.Name) {
|
||||||
|
name = preferredProductTitle(in.GTIN, n2)
|
||||||
|
} else {
|
||||||
|
name = preferredProductTitle(in.GTIN, n2, name, in.Name, in.PriorProcessedName)
|
||||||
|
}
|
||||||
desc = preferredProductDescription(name, d2)
|
desc = preferredProductDescription(name, d2)
|
||||||
mt2, md2 := metaFieldsFromEnhanceObj(obj2)
|
mt2, md2 := metaFieldsFromEnhanceObj(obj2)
|
||||||
if mt2 != "" {
|
if mt2 != "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user