diff --git a/apps/api/cmd/seed-a1/category_prompts.go b/apps/api/cmd/seed-a1/category_prompts.go index c6243b7..2933c71 100644 --- a/apps/api/cmd/seed-a1/category_prompts.go +++ b/apps/api/cmd/seed-a1/category_prompts.go @@ -188,7 +188,8 @@ func applyCategoryPrompts(ctx context.Context, pg *pgxpool.Pool, companyID uuid. byNorm := make(map[string]string, len(file.Entries)) byUnique := make(map[string]string, len(file.Entries)) // Overlay: split each legacy combined Name+Description prompt into role-sectioned - // enhance USER text (Title / Description / Meta / Attributes). Unique title / + // enhance USER text (Title / Description / Meta) holding exactly the legacy + // per-category text — no schema/role boilerplate. Unique title / // description / meta formulas stay in title_template / description_template. // Stored under "sl" + "*" so prompt->>'sl' and LangPromptAny both resolve; // language stays via {{language}}. @@ -207,10 +208,13 @@ func applyCategoryPrompts(ctx context.Context, pg *pgxpool.Pool, companyID uuid. } if aiprompts.IsLegacyCombinedEnhancePrompt(raw) { canonical = prepareCategoryPrompt(aiprompts.SplitLegacyCombinedEnhancePrompt(raw)) - } else if aiprompts.CategoryEnhanceHasRoleSections(raw) { + } else if aiprompts.CategoryEnhanceHasRoleSections(raw) && !aiprompts.CategoryEnhancePromptNeedsRepair(raw) { canonical = prepareCategoryPrompt(raw) } else { - canonical = prepareCategoryPrompt(aiprompts.CategoryEnhanceUserTemplate) + // No usable per-category content — never store template boilerplate; + // the category keeps the company default. + out.Skipped++ + continue } if canonical == "" { out.Skipped++ diff --git a/apps/api/internal/aiprompts/kinds.go b/apps/api/internal/aiprompts/kinds.go index edeeae0..0dcafd4 100644 --- a/apps/api/internal/aiprompts/kinds.go +++ b/apps/api/internal/aiprompts/kinds.go @@ -84,12 +84,12 @@ Category: {{category}} Attrs: {{attrs}}` // CategoryEnhancePromptNeedsRepair reports whether a stored categories.prompt value -// should be rewritten into role-sectioned enhance overlay form. Empty prompts are -// left alone. Canonical CategoryEnhanceUserTemplate and per-category overlays that -// already carry Title/Description/Meta markers are OK — equality with the shared -// template is not required (legacy splits keep Slovenian rules). Prompts still -// carrying a retired "--- Attributes ---" role section are rewritten so attribute -// prompting stays out of category prompts. +// should be rewritten into clean role-sectioned overlay form (or cleared). Empty +// prompts are left alone. A clean overlay carries Title/Description/Meta markers +// with ONLY the category's own instruction text — schema/role machine boilerplate +// (old seed versions stored the canonical template text, "Build JSON …" role +// instructions, and a retired "--- Attributes ---" section) marks a prompt for +// repair so the prompt editor shows exactly the per-category rules. func CategoryEnhancePromptNeedsRepair(prompt string) bool { p := strings.TrimSpace(prompt) if p == "" { @@ -101,7 +101,12 @@ func CategoryEnhancePromptNeedsRepair(prompt string) bool { if !CategoryEnhanceHasRoleSections(p) { return true } - return strings.Contains(strings.ToLower(p), strings.ToLower(SectionAttributesStart)) + lower := strings.ToLower(p) + if strings.Contains(lower, strings.ToLower(SectionAttributesStart)) { + return true + } + return strings.Contains(lower, "your reply is parsed as json") || + strings.Contains(lower, `build json "`) } // BuiltInDefaults match the previous hardcoded system prompts, with structured user templates. diff --git a/apps/api/internal/aiprompts/kinds_test.go b/apps/api/internal/aiprompts/kinds_test.go index b6c71fa..24ffa6f 100644 --- a/apps/api/internal/aiprompts/kinds_test.go +++ b/apps/api/internal/aiprompts/kinds_test.go @@ -43,17 +43,16 @@ func TestCategoryEnhancePromptNeedsRepair(t *testing.T) { if CategoryEnhancePromptNeedsRepair("") { t.Fatal("empty should not need repair") } - if CategoryEnhancePromptNeedsRepair(CategoryEnhanceUserTemplate) { - t.Fatal("canonical template should be idempotent") - } - if CategoryEnhancePromptNeedsRepair(" " + CategoryEnhanceUserTemplate + "\n") { - t.Fatal("whitespace-trimmed canonical should be idempotent") + // A STORED copy of the built-in template is boilerplate pollution — repair + // clears it so the category falls back to the company default. + if !CategoryEnhancePromptNeedsRepair(CategoryEnhanceUserTemplate) { + t.Fatal("stored canonical template text should need repair (cleared to default)") } legacy := `Ustvari nov opis\n

foo

\nAttrs missing` if !CategoryEnhancePromptNeedsRepair(legacy) { t.Fatal("legacy HTML prompt should need repair") } - // Per-category sectioned overlay (Slovenian rules) must not force re-repair. + // Per-category sectioned overlay (pure Slovenian rules) must not force re-repair. split := SplitLegacyCombinedEnhancePrompt(`GPT predloga: {Napiši tip izdelka} {140 znakov} @@ -61,6 +60,9 @@ func TestCategoryEnhancePromptNeedsRepair(t *testing.T) { if CategoryEnhancePromptNeedsRepair(split) { t.Fatal("sectioned split overlay should be idempotent") } + if CategoryEnhancePromptNeedsRepair(" " + split + "\n") { + t.Fatal("whitespace-trimmed split overlay should be idempotent") + } } func TestBuiltInProductEnhanceUsesSharedUserTemplate(t *testing.T) { diff --git a/apps/api/internal/aiprompts/legacy_split.go b/apps/api/internal/aiprompts/legacy_split.go index 8c1ae42..ab3d4cb 100644 --- a/apps/api/internal/aiprompts/legacy_split.go +++ b/apps/api/internal/aiprompts/legacy_split.go @@ -74,61 +74,39 @@ func ParseLegacyCombinedEnhancePrompt(prompt string) LegacyEnhanceParts { } // SplitLegacyCombinedEnhancePrompt rewrites a legacy combined name+description(+meta) -// prompt into CategoryEnhanceUserTemplate role sections, preserving Slovenian -// naming / HTML / meta intent as category-specific rules under Title / Description / Meta. -// Non-legacy input returns CategoryEnhanceUserTemplate (canonical shared overlay). +// prompt into role sections carrying ONLY the original per-category text — the +// Slovenian naming formula under Title, the HTML body structure under Description, +// the SEO instruction under Meta. No schema/role boilerplate is stored: JSON +// schema, role framing, and {{name}}/{{description}}/{{category}}/{{attrs}} +// product context are all supplied at render time (system template + +// processing.ensureCategoryEnhanceUserContext). Non-legacy input returns "" — +// callers leave the category without an override (company default applies). func SplitLegacyCombinedEnhancePrompt(prompt string) string { parts := ParseLegacyCombinedEnhancePrompt(prompt) if !parts.WasLegacy { - return strings.TrimSpace(CategoryEnhanceUserTemplate) + return "" } return BuildCategoryEnhanceOverlay(parts) } // BuildCategoryEnhanceOverlay composes a role-sectioned enhance USER overlay from -// extracted legacy parts (or empty parts → canonical template text with no extra rules). +// extracted legacy parts — the stored text is exactly the split legacy content, +// matching what a user would type into the Title/Description/Meta prompt areas. func BuildCategoryEnhanceOverlay(parts LegacyEnhanceParts) string { var b strings.Builder - b.WriteString(`Your reply is parsed as JSON {"name":"string","description":"string","meta_title":"string","meta_description":"string","attrs":{}} only (system schema). Write all string fields in {{language}} (do not hardcode a language).`) - b.WriteString("\n\n") - - b.WriteString(SectionTitleStart) - b.WriteByte('\n') - b.WriteString(TitleRoleInstruction) - b.WriteByte('\n') - if rules := strings.TrimSpace(parts.TitleRules); rules != "" { - b.WriteString("Category naming rules (preserve intent): ") - b.WriteString(rules) + section := func(start, end, body string) { + b.WriteString(start) b.WriteByte('\n') + if body = strings.TrimSpace(body); body != "" { + b.WriteString(body) + b.WriteByte('\n') + } + b.WriteString(end) + b.WriteString("\n\n") } - b.WriteString("Name: {{name}}\n") - b.WriteString(SectionTitleEnd) - b.WriteString("\n\n") - - b.WriteString(SectionDescriptionStart) - b.WriteString("\nRole: description. Build JSON \"description\": product body HTML only (not SEO meta). 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 (