fix
This commit is contained in:
@@ -48,10 +48,12 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
||||
in.Name,
|
||||
in.PriorProcessedName,
|
||||
)
|
||||
out.Description = preferredProductDescription(out.Name,
|
||||
// Original is supplier copy only. PriorProcessedDescription is the last
|
||||
// ENRICHED text — using it here made Review report "Matched" against copy
|
||||
// the feed never contained.
|
||||
out.Description = supplierOriginalDescription(out.Name,
|
||||
stringFromAny(normalized["description"]),
|
||||
in.Description,
|
||||
in.PriorProcessedDescription,
|
||||
)
|
||||
// mapped_data.category / category_unique_id (unique_id codes) win.
|
||||
applyCategoryFromMapped(&out, normalized, in.Mapped, in.Raw)
|
||||
@@ -104,7 +106,7 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
||||
in.Name,
|
||||
in.PriorProcessedName,
|
||||
)
|
||||
out.Description = preferredProductDescription(out.Name,
|
||||
out.Description = supplierOriginalDescription(out.Name,
|
||||
stringFromAny(normalized["description"]),
|
||||
out.Description,
|
||||
in.Description,
|
||||
@@ -451,7 +453,7 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
||||
out.ProcessedDescription = out.Description
|
||||
}
|
||||
// Timeout/error/empty/formula-miss: always synthesize a factual fallback when a title exists.
|
||||
_, failDescTpl := categoryFormulasFor(in, out.Category)
|
||||
failDescTpl := effectiveDescriptionTemplateFor(in, out)
|
||||
if out.ProcessedName != "" && descriptionNeedsEnhanceRepair(out.ProcessedDescription, failDescTpl, out.ProcessedName, out.Name) {
|
||||
if synth := synthesizeProductDescription(out.ProcessedName, displayCat, primary, enhanceAttrs, failDescTpl); synth != "" {
|
||||
out.ProcessedDescription = synth
|
||||
@@ -528,17 +530,22 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
||||
}
|
||||
}
|
||||
|
||||
// Pipelines without StepCategorize (enhance_only / normalize_only) still run
|
||||
// vector + LLM categorize when category is empty so enhance is not fed a blank.
|
||||
// Pipelines without StepCategorize (normalize_only) still run vector + LLM
|
||||
// categorize when category is empty so downstream steps are not fed a blank.
|
||||
if !stepsContain(steps, StepCategorize) {
|
||||
runCategorizeStep(ctx, e, companyID, &out, in, categoryNames, policy)
|
||||
noteMissingCategory(&out, policy, e != nil && e.Vector != nil && e.Vector.Enabled())
|
||||
}
|
||||
// Record why category stayed empty for every pipeline, including the ones that
|
||||
// now categorize inside the loop (enhance / title / description).
|
||||
noteMissingCategory(&out, policy, e != nil && e.Vector != nil && e.Vector.Enabled())
|
||||
preserveCategoryIfEmpty(&out, in.PriorCategory)
|
||||
syncCategoryName(&out, in.CategoryNamesByUID)
|
||||
out.Name = preferredProductTitle(in.GTIN, out.Name, out.ProcessedName, in.Name, in.PriorProcessedName)
|
||||
out.ProcessedName = preferredProductTitle(in.GTIN, out.ProcessedName, out.Name, in.PriorProcessedName, in.Name)
|
||||
out.Description = preferredProductDescription(out.Name, out.Description, out.ProcessedDescription, in.Description)
|
||||
// Original stays supplier copy; only the enriched side may borrow from it.
|
||||
// Filling Original from ProcessedName/Description is what produced Review rows
|
||||
// that read "Matched" while the Feed tab still showed the real supplier text.
|
||||
out.Name = preferredProductTitle(in.GTIN, out.Name, in.Name)
|
||||
out.ProcessedName = finalProcessedTitle(in, &out)
|
||||
out.Description = supplierOriginalDescription(out.Name, out.Description, in.Description)
|
||||
out.ProcessedDescription = preferredProductDescription(out.ProcessedName, out.ProcessedDescription, out.Description, in.PriorProcessedDescription)
|
||||
if out.ProcessedName == "" {
|
||||
out.ProcessedName = out.Name
|
||||
@@ -549,7 +556,7 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
||||
displayCat := categoryDisplayLabel(out)
|
||||
// After vector/mapped category resolution, formulas may key for the first time —
|
||||
// repair short prose / title echo that ignored A1 description_template.
|
||||
_, finalDescTpl := categoryFormulasFor(in, out.Category)
|
||||
finalDescTpl := effectiveDescriptionTemplateFor(in, out)
|
||||
finalRepaired := false
|
||||
if descriptionNeedsEnhanceRepair(out.ProcessedDescription, finalDescTpl, out.ProcessedName, out.Name) {
|
||||
if synth := synthesizeProductDescription(out.ProcessedName, displayCat, in.Language, out.Attributes, finalDescTpl); synth != "" {
|
||||
@@ -684,7 +691,12 @@ func noteMissingCategory(out *StepResult, policy StepPolicy, vectorEnabled bool)
|
||||
func resolveSteps(processingType string) []string {
|
||||
switch strings.ToLower(strings.TrimSpace(processingType)) {
|
||||
case "enhance", "enhance_only", "enhance-only", "title", "description":
|
||||
return []string{StepNormalize, StepAIEnhance}
|
||||
// parse_specs/fill_fields/categorize feed ai_enhance — they never rewrite
|
||||
// title/description themselves. Without them enhance ran with empty Attrs
|
||||
// and (for feeds without a mapped category) no category, so the category
|
||||
// title/description formula could not key and the model echoed the feed.
|
||||
// categorize is a no-op when the category is already set.
|
||||
return []string{StepNormalize, StepParseSpecs, StepFillFields, StepCategorize, StepAIEnhance}
|
||||
case "attributes", "attributes_only", "specs", "specifications":
|
||||
return []string{StepNormalize, StepParseSpecs, StepFillFields}
|
||||
case "eprel", "eprel_only":
|
||||
@@ -779,10 +791,10 @@ func normalizeTitleCompare(s string) string {
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
func titleNeedsRewriteRetry(in ProductInput, name string) bool {
|
||||
if !titlesAreEquivalent(name, in.Name) {
|
||||
return false
|
||||
}
|
||||
// categoryRequiresTitleRewrite reports that the category asks enhance for a NEW
|
||||
// product name — a structured title_template, or free-form Title instructions that
|
||||
// spell out a naming formula (legacy A1 <name>{…}</name>).
|
||||
func categoryRequiresTitleRewrite(in ProductInput) bool {
|
||||
if aiprompts.CategoryTitlePromptRequiresRewrite(in.CategoryEnhancePrompt) {
|
||||
return true
|
||||
}
|
||||
@@ -792,6 +804,62 @@ func titleNeedsRewriteRetry(in ProductInput, name string) bool {
|
||||
return FormatTitleSectionConstraint(in.CategoryEnhancePrompt) != ""
|
||||
}
|
||||
|
||||
func titleNeedsRewriteRetry(in ProductInput, name string) bool {
|
||||
return titlesAreEquivalent(name, in.Name) && categoryRequiresTitleRewrite(in)
|
||||
}
|
||||
|
||||
// formulaAwareTitle keeps a formula-built name even when it is shorter than — and a
|
||||
// prefix of — the supplier Name. preferredProductTitle's brand-stub rule would
|
||||
// otherwise prefer the longer feed title and silently undo the Title formula
|
||||
// (A1 formulas like "znamka tip model" often are a prefix of the feed name).
|
||||
func formulaAwareTitle(in ProductInput, llmName string) string {
|
||||
llmName = strings.TrimSpace(llmName)
|
||||
if llmName != "" && llmName != "<nil>" && !isPromptLabelTitle(llmName) &&
|
||||
!titlesAreEquivalent(llmName, in.Name) && categoryRequiresTitleRewrite(in) {
|
||||
return preferredProductTitle(in.GTIN, llmName)
|
||||
}
|
||||
return preferredProductTitle(in.GTIN, llmName, in.Name, in.PriorProcessedName)
|
||||
}
|
||||
|
||||
// finalProcessedTitle settles ProcessedName after category resolution. A name the
|
||||
// enhance step built from an active category Title formula is kept as-is; only an
|
||||
// empty/unusable one falls back to the supplier title.
|
||||
func finalProcessedTitle(in ProductInput, out *StepResult) string {
|
||||
processed := strings.TrimSpace(out.ProcessedName)
|
||||
if processed != "" && processed != "<nil>" && !isPromptLabelTitle(processed) &&
|
||||
!titlesAreEquivalent(processed, out.Name) {
|
||||
titleTpl, _ := categoryFormulasFor(in, out.Category)
|
||||
formulaIn := in
|
||||
formulaIn.TitleTemplate = titleTpl
|
||||
if formulaIn.CategoryEnhancePrompt == "" {
|
||||
formulaIn.CategoryEnhancePrompt = categoryEnhancePromptFor(
|
||||
in.CategoryPromptsByLang, out.Category, in.Language, in.Language)
|
||||
}
|
||||
if categoryRequiresTitleRewrite(formulaIn) {
|
||||
return preferredProductTitle(in.GTIN, processed)
|
||||
}
|
||||
}
|
||||
return preferredProductTitle(in.GTIN, out.ProcessedName, out.Name, in.PriorProcessedName, in.Name)
|
||||
}
|
||||
|
||||
// supplierOriginalDescription picks the Original (pre-enrich) description from
|
||||
// supplier candidates only. When every candidate trips a quality filter it still
|
||||
// returns the raw feed text — Original is meant to mirror the feed, and leaving it
|
||||
// blank (or borrowing enriched copy) is what made Review show a false "Matched".
|
||||
func supplierOriginalDescription(title string, candidates ...string) string {
|
||||
if desc := preferredProductDescription(title, candidates...); desc != "" {
|
||||
return desc
|
||||
}
|
||||
for _, c := range candidates {
|
||||
c = strings.TrimSpace(c)
|
||||
if c == "" || c == "<nil>" || isPromptLabelTitle(c) {
|
||||
continue
|
||||
}
|
||||
return SanitizeOutput(c)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// enhanceHashForceReason returns why a matching prior enhance_input_hash must not
|
||||
// skip the LLM (empty = safe to reuse as ai_enhance_unchanged).
|
||||
func enhanceHashForceReason(in ProductInput) string {
|
||||
@@ -831,13 +899,19 @@ func normalizeDescCompare(s string) string {
|
||||
}
|
||||
|
||||
func (e *Engine) enhance(ctx context.Context, in ProductInput, category string, attrs map[string]any) (string, string, int, any, error) {
|
||||
catUID := strings.TrimSpace(in.CategoryUniqueID)
|
||||
catName := strings.TrimSpace(category)
|
||||
if in.CategoryDisplayName == "" {
|
||||
in.CategoryDisplayName = catName
|
||||
}
|
||||
// Fill the platform default before anything reads the formula, so the prompt the
|
||||
// model receives and the gate its reply is judged against are the same formula.
|
||||
in = withDefaultCategoryFormula(in)
|
||||
// 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)
|
||||
catName := strings.TrimSpace(category)
|
||||
sysTpl, userTpl := resolveProductPromptTemplates(in)
|
||||
hash := HashEnhanceInput(category, in.Name, in.Description, in.BrandPrompt, in.Language, sysTpl, userTpl, attrs)
|
||||
if e == nil || e.Completer == nil {
|
||||
@@ -967,7 +1041,7 @@ func (e *Engine) enhance(ctx context.Context, in ProductInput, category string,
|
||||
return name, desc, comp.TotalTokens, meta, nil
|
||||
}
|
||||
|
||||
name := preferredProductTitle(in.GTIN, llmName, in.Name, in.PriorProcessedName)
|
||||
name := formulaAwareTitle(in, llmName)
|
||||
// Quality-gate on LLM description alone (do not absorb originals yet).
|
||||
desc := preferredProductDescription(name, llmDesc)
|
||||
didFormulaRetry := false
|
||||
@@ -1582,7 +1656,9 @@ func preferredProductDescription(title string, candidates ...string) string {
|
||||
if containsWeakFillerPhrase(c) {
|
||||
continue
|
||||
}
|
||||
if company.LooksLikeHeuristicSynthesize(c) {
|
||||
// Strict test only: this drops the candidate outright, so ambiguous
|
||||
// skeleton wording must not disqualify real formula copy.
|
||||
if company.IsInventFallbackDescription(c) {
|
||||
continue
|
||||
}
|
||||
return SanitizeOutput(c)
|
||||
|
||||
Reference in New Issue
Block a user