fix
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: up down api worker backend web migrate sqlc test test-api check-web vet seed seed-woo mock-woo health setup cutover-rehearsal build
|
.PHONY: up down api worker backend web migrate sqlc test test-api check-web vet seed seed-woo mock-woo health setup cutover-rehearsal build seed-defaults check-defaults formula-e2e
|
||||||
|
|
||||||
# Start local Postgres only (:5433 — pairs with host npm run setup / npm run dev)
|
# Start local Postgres only (:5433 — pairs with host npm run setup / npm run dev)
|
||||||
up:
|
up:
|
||||||
@@ -72,3 +72,19 @@ check-web:
|
|||||||
# Low-memory production build (web then Go api+worker binaries; ~4GB-safe defaults)
|
# Low-memory production build (web then Go api+worker binaries; ~4GB-safe defaults)
|
||||||
build:
|
build:
|
||||||
node scripts/build.mjs
|
node scripts/build.mjs
|
||||||
|
|
||||||
|
# Regenerate the English platform-default category formulas from the A1 seed
|
||||||
|
# (writes apps/api/internal/aiprompts/defaults/default-category-prompts.json).
|
||||||
|
seed-defaults:
|
||||||
|
node scripts/seed/build-default-category-prompts.mjs
|
||||||
|
|
||||||
|
# Fail if the embedded English defaults drift from the A1 seed + translation table.
|
||||||
|
check-defaults:
|
||||||
|
node scripts/seed/build-default-category-prompts.mjs --check
|
||||||
|
|
||||||
|
# Local proof that category formulas drive generation (needs Postgres + mock-llm):
|
||||||
|
# make up && go run ./cmd/mock-llm &
|
||||||
|
# make formula-e2e
|
||||||
|
formula-e2e:
|
||||||
|
cd apps/api && go run ./cmd/formula-e2e -company "A1 Slovenija" -limit 5 -llm-base http://127.0.0.1:18767/v1
|
||||||
|
cd apps/api && go run ./cmd/formula-e2e -new-tenant -llm-base http://127.0.0.1:18767/v1
|
||||||
|
|||||||
@@ -0,0 +1,409 @@
|
|||||||
|
// Command formula-e2e runs the real processing pipeline against the local database
|
||||||
|
// and reports whether category formulas actually drove the generated copy.
|
||||||
|
//
|
||||||
|
// It is a local proof, not a test double: it builds processing.Pipeline the way
|
||||||
|
// cmd/worker does (same AI resolution, same prompts service, same Engine), starts a
|
||||||
|
// real job, processes it, and then reads processed_products back.
|
||||||
|
//
|
||||||
|
// go run ./cmd/mock-llm -addr 127.0.0.1:18767 &
|
||||||
|
// go run ./cmd/formula-e2e -company "A1 Slovenija" -limit 3
|
||||||
|
// go run ./cmd/formula-e2e -company "A1 Slovenija" -gtin 8022068075495
|
||||||
|
// go run ./cmd/formula-e2e -new-tenant # English defaults on a fresh company
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprovider"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/catalog"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/platformsettings"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
companyName := flag.String("company", "A1 Slovenija", "company name to process")
|
||||||
|
gtin := flag.String("gtin", "", "specific GTIN to process (default: first products with a category)")
|
||||||
|
limit := flag.Int("limit", 2, "how many products to process")
|
||||||
|
ptype := flag.String("type", "full", "processing_type")
|
||||||
|
newTenant := flag.Bool("new-tenant", false, "create a throwaway English tenant + category + product and process that")
|
||||||
|
keep := flag.Bool("keep", false, "keep the throwaway tenant instead of deleting it")
|
||||||
|
llmBase := flag.String("llm-base", "", "override the resolved LLM base URL (e.g. http://127.0.0.1:18767/v1 for cmd/mock-llm)")
|
||||||
|
llmKey := flag.String("llm-key", "local-test", "API key for -llm-base")
|
||||||
|
llmModel := flag.String("llm-model", "mock-llm", "model id for -llm-base")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("config: %v", err)
|
||||||
|
}
|
||||||
|
pool, err := pgxpool.New(ctx, cfg.DatabaseURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("db: %v", err)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
if err := pool.Ping(ctx); err != nil {
|
||||||
|
log.Fatalf("db ping: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline := newWorkerLikePipeline(ctx, pool, cfg)
|
||||||
|
if strings.TrimSpace(*llmBase) != "" {
|
||||||
|
// Everything else stays production-identical; only the provider endpoint is
|
||||||
|
// pinned so a local run does not depend on the configured remote model.
|
||||||
|
pipeline.AI = fixedCompleter{
|
||||||
|
client: processing.NewOpenAIClient(*llmKey, *llmBase, *llmModel, 0, 2),
|
||||||
|
}
|
||||||
|
log.Printf("LLM override: base=%s model=%s", *llmBase, *llmModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *newTenant {
|
||||||
|
if err := runNewTenant(ctx, pool, pipeline, *keep); err != nil {
|
||||||
|
log.Fatalf("new tenant: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := runExisting(ctx, pool, pipeline, *companyName, *gtin, *limit, *ptype); err != nil {
|
||||||
|
log.Fatalf("run: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixedCompleter pins every job to one endpoint (local mock-llm) while leaving the
|
||||||
|
// rest of the pipeline exactly as the worker builds it.
|
||||||
|
type fixedCompleter struct{ client processing.Completer }
|
||||||
|
|
||||||
|
func (f fixedCompleter) ResolveCompleter(context.Context, uuid.UUID) (processing.Completer, string, bool, error) {
|
||||||
|
return f.client, processing.AIProviderInternal, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fixedCompleter) ResolveCompleterForRole(ctx context.Context, id uuid.UUID, _ string) (processing.Completer, string, bool, error) {
|
||||||
|
return f.ResolveCompleter(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newWorkerLikePipeline mirrors cmd/worker/main.go so this proof exercises the same
|
||||||
|
// code path production does.
|
||||||
|
func newWorkerLikePipeline(ctx context.Context, pool *pgxpool.Pool, cfg config.Config) *processing.Pipeline {
|
||||||
|
platSettings := platformsettings.NewService(pool, platformsettings.EnvConfig{
|
||||||
|
AppEncryptionKey: cfg.AppEncryptionKey,
|
||||||
|
CredentialsEncryptionKey: cfg.CredentialsEncryptionKey,
|
||||||
|
TokenSigningSecret: cfg.TokenSigningSecret,
|
||||||
|
DatabaseURL: cfg.DatabaseURL,
|
||||||
|
OpenAIAPIKey: cfg.OpenAIAPIKey,
|
||||||
|
OpenAIBaseURL: cfg.OpenAIBaseURL,
|
||||||
|
OpenAIModel: cfg.OpenAIModel,
|
||||||
|
})
|
||||||
|
aiSvc := aiprovider.NewService(pool, aiprovider.EnvConfig{
|
||||||
|
AppEncryptionKey: cfg.AppEncryptionKey,
|
||||||
|
CredentialsEncryptionKey: cfg.CredentialsEncryptionKey,
|
||||||
|
TokenSigningSecret: cfg.TokenSigningSecret,
|
||||||
|
DatabaseURL: cfg.DatabaseURL,
|
||||||
|
OpenAIAPIKey: cfg.OpenAIAPIKey,
|
||||||
|
OpenAIBaseURL: cfg.OpenAIBaseURL,
|
||||||
|
OpenAIModel: cfg.OpenAIModel,
|
||||||
|
ProcessingRPM: cfg.ProcessingRPM,
|
||||||
|
ProcessingMaxRetries: cfg.ProcessingMaxRetries,
|
||||||
|
})
|
||||||
|
aiSvc.Platform = platSettings
|
||||||
|
p := processing.NewPipeline(pool)
|
||||||
|
p.AI = aiSvc
|
||||||
|
p.Prompts = aiprompts.NewService(pool)
|
||||||
|
p.Limiter = nil
|
||||||
|
p.Engine = &processing.Engine{
|
||||||
|
Vector: processing.NoopVectorCategorizer{},
|
||||||
|
ProviderMode: processing.AIProviderInternal,
|
||||||
|
}
|
||||||
|
if oi, err := platSettings.ResolveOpenAI(ctx); err != nil {
|
||||||
|
log.Printf("WARNING: platform OpenAI resolve failed: %v", err)
|
||||||
|
} else if strings.TrimSpace(oi.APIKey) == "" {
|
||||||
|
log.Printf("WARNING: platform OpenAI unset — AI enhance will be skipped")
|
||||||
|
} else {
|
||||||
|
log.Printf("OpenAI: source=%s base=%s model=%s", oi.Source, oi.BaseURL, oi.Model)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func runExisting(ctx context.Context, pool *pgxpool.Pool, p *processing.Pipeline, companyName, gtin string, limit int, ptype string) error {
|
||||||
|
var companyID uuid.UUID
|
||||||
|
var lang string
|
||||||
|
err := pool.QueryRow(ctx, `
|
||||||
|
SELECT id, COALESCE(language, '') FROM companies WHERE name = $1`, companyName).Scan(&companyID, &lang)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("company %q: %w", companyName, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("\n=== %s (%s, language=%s)\n", companyName, companyID, lang)
|
||||||
|
|
||||||
|
var rawIDs []uuid.UUID
|
||||||
|
if strings.TrimSpace(gtin) != "" {
|
||||||
|
rows, err := pool.Query(ctx, `
|
||||||
|
SELECT id FROM raw_products WHERE company_id = $1 AND gtin = $2 LIMIT $3`,
|
||||||
|
companyID, strings.TrimSpace(gtin), limit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rawIDs, err = collectIDs(rows)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rows, err := pool.Query(ctx, `
|
||||||
|
SELECT id FROM raw_products
|
||||||
|
WHERE company_id = $1
|
||||||
|
AND COALESCE(NULLIF(trim(mapped_data->>'category'), ''), '') <> ''
|
||||||
|
AND length(COALESCE(mapped_data->>'description', '')) > 120
|
||||||
|
ORDER BY id
|
||||||
|
LIMIT $2`, companyID, limit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rawIDs, err = collectIDs(rows)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rawIDs) == 0 {
|
||||||
|
return fmt.Errorf("no matching raw products")
|
||||||
|
}
|
||||||
|
return processAndReport(ctx, pool, p, companyID, rawIDs, ptype)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAndReport(ctx context.Context, pool *pgxpool.Pool, p *processing.Pipeline, companyID uuid.UUID, rawIDs []uuid.UUID, ptype string) error {
|
||||||
|
// Clear prior enhance hashes so the run cannot be skipped as "unchanged".
|
||||||
|
if _, err := pool.Exec(ctx, `
|
||||||
|
UPDATE processed_products
|
||||||
|
SET field_sources = field_sources - 'enhance_input_hash'
|
||||||
|
WHERE company_id = $1 AND raw_product_id = ANY($2)`, companyID, rawIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var userID uuid.UUID
|
||||||
|
if err := pool.QueryRow(ctx, `
|
||||||
|
SELECT user_id FROM memberships WHERE company_id = $1 ORDER BY created_at LIMIT 1`, companyID).Scan(&userID); err != nil {
|
||||||
|
return fmt.Errorf("membership: %w", err)
|
||||||
|
}
|
||||||
|
jobs, err := p.StartJob(ctx, companyID, userID, rawIDs, ptype)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("start job: %w", err)
|
||||||
|
}
|
||||||
|
for _, j := range jobs {
|
||||||
|
if err := p.ProcessJob(ctx, j.ID); err != nil {
|
||||||
|
return fmt.Errorf("process job %s: %w", j.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return report(ctx, pool, companyID, rawIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func report(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID, rawIDs []uuid.UUID) error {
|
||||||
|
rows, err := pool.Query(ctx, `
|
||||||
|
SELECT COALESCE(rp.gtin, ''),
|
||||||
|
COALESCE(pp.category, ''),
|
||||||
|
COALESCE(rp.mapped_data->>'name', ''),
|
||||||
|
COALESCE(rp.mapped_data->>'description', ''),
|
||||||
|
COALESCE(pp.processed_name, ''),
|
||||||
|
COALESCE(pp.processed_description, ''),
|
||||||
|
COALESCE(pp.field_sources::text, '{}'),
|
||||||
|
COALESCE(pp.processed_attributes::text, '{}')
|
||||||
|
FROM raw_products rp
|
||||||
|
LEFT JOIN processed_products pp ON pp.raw_product_id = rp.id AND pp.company_id = rp.company_id
|
||||||
|
WHERE rp.company_id = $1 AND rp.id = ANY($2)`, companyID, rawIDs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
fails := 0
|
||||||
|
for rows.Next() {
|
||||||
|
var gtin, category, feedName, feedDesc, name, desc, sources, attrs string
|
||||||
|
if err := rows.Scan(>in, &category, &feedName, &feedDesc, &name, &desc, &sources, &attrs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("\n---------------- GTIN %s category=%q\n", gtin, category)
|
||||||
|
fmt.Printf("FEED name : %s\n", trunc(feedName, 140))
|
||||||
|
fmt.Printf("FEED desc : %s\n", trunc(plain(feedDesc), 200))
|
||||||
|
fmt.Printf("NEW name : %s\n", trunc(name, 140))
|
||||||
|
fmt.Printf("NEW desc : %s\n", trunc(desc, 400))
|
||||||
|
fmt.Printf("attrs : %s\n", trunc(attrs, 200))
|
||||||
|
fmt.Printf("sources : %s\n", trunc(sources, 240))
|
||||||
|
|
||||||
|
var problems []string
|
||||||
|
if strings.TrimSpace(name) == "" || strings.TrimSpace(desc) == "" {
|
||||||
|
problems = append(problems, "empty processed output")
|
||||||
|
}
|
||||||
|
if strings.EqualFold(strings.TrimSpace(name), strings.TrimSpace(feedName)) {
|
||||||
|
problems = append(problems, "NAME copied from feed")
|
||||||
|
}
|
||||||
|
if strings.EqualFold(strings.TrimSpace(plain(desc)), strings.TrimSpace(plain(feedDesc))) {
|
||||||
|
problems = append(problems, "DESCRIPTION copied from feed")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(desc), "<h") && !strings.Contains(strings.ToLower(desc), "<p") {
|
||||||
|
problems = append(problems, "description is not formula HTML")
|
||||||
|
}
|
||||||
|
if len(problems) == 0 {
|
||||||
|
fmt.Printf("VERDICT : OK — generated from formula\n")
|
||||||
|
} else {
|
||||||
|
fails++
|
||||||
|
fmt.Printf("VERDICT : FAIL — %s\n", strings.Join(problems, "; "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fails > 0 {
|
||||||
|
// Returned, never os.Exit: the throwaway-tenant cleanup is a defer.
|
||||||
|
return fmt.Errorf("%d product(s) did not generate from their category formula", fails)
|
||||||
|
}
|
||||||
|
fmt.Printf("\nall products generated from their category formula\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runNewTenant proves the platform default: a brand-new company and category get
|
||||||
|
// English formulas at create time and enhance into that shape.
|
||||||
|
func runNewTenant(ctx context.Context, pool *pgxpool.Pool, p *processing.Pipeline, keep bool) error {
|
||||||
|
suffix := uuid.NewString()[:8]
|
||||||
|
companyName := "formula-e2e-" + suffix
|
||||||
|
var companyID uuid.UUID
|
||||||
|
if err := pool.QueryRow(ctx, `
|
||||||
|
INSERT INTO companies (name, language, content_languages)
|
||||||
|
VALUES ($1, 'en', ARRAY['en']) RETURNING id`, companyName).Scan(&companyID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !keep {
|
||||||
|
defer func() {
|
||||||
|
if _, err := pool.Exec(context.Background(), `DELETE FROM companies WHERE id = $1`, companyID); err != nil {
|
||||||
|
log.Printf("cleanup: %v", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\ncleaned up throwaway tenant %s\n", companyName)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
var userID uuid.UUID
|
||||||
|
if err := pool.QueryRow(ctx, `SELECT id FROM users ORDER BY created_at LIMIT 1`).Scan(&userID); err != nil {
|
||||||
|
return fmt.Errorf("need at least one user: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := pool.Exec(ctx, `
|
||||||
|
INSERT INTO memberships (user_id, company_id, role) VALUES ($1, $2, 'admin')
|
||||||
|
ON CONFLICT DO NOTHING`, userID, companyID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := grantAIPlan(ctx, pool, companyID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category created through the real service — that is where defaults are applied.
|
||||||
|
svc := &catalog.Service{Pool: pool}
|
||||||
|
cat, err := svc.CreateCategory(ctx, companyID, "High chairs", "high-chairs", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create category: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("\n=== new tenant %s (%s)\n", companyName, companyID)
|
||||||
|
printCategoryFormulas(cat)
|
||||||
|
|
||||||
|
mapped := map[string]any{
|
||||||
|
"name": "BRUNNER folding camping chair ONE SHOT grey black 0404164N.C20",
|
||||||
|
"description": "An elegant and very light chair designed for art directors. Folding aluminium frame, comfortable seat and armrests.",
|
||||||
|
"brand": "BRUNNER",
|
||||||
|
"gtin": "8022068075495",
|
||||||
|
"category": "high-chairs",
|
||||||
|
"specifications": map[string]any{
|
||||||
|
"Colour": "grey black",
|
||||||
|
"Material": "aluminium",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mappedJSON, _ := json.Marshal(mapped)
|
||||||
|
var rawID uuid.UUID
|
||||||
|
if err := pool.QueryRow(ctx, `
|
||||||
|
INSERT INTO raw_products (company_id, gtin, mapped_data, raw_data)
|
||||||
|
VALUES ($1, $2, $3::jsonb, '{}'::jsonb) RETURNING id`,
|
||||||
|
companyID, "8022068075495", string(mappedJSON)).Scan(&rawID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return processAndReport(ctx, pool, p, companyID, []uuid.UUID{rawID}, "full")
|
||||||
|
}
|
||||||
|
|
||||||
|
func grantAIPlan(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) error {
|
||||||
|
// Reuse whichever plan the A1 tenant is on so entitlements (can_use_ai) match a
|
||||||
|
// real paid tenant rather than a hand-built feature map.
|
||||||
|
var planID int64
|
||||||
|
err := pool.QueryRow(ctx, `
|
||||||
|
SELECT cp.plan_id FROM company_plans cp
|
||||||
|
JOIN companies c ON c.id = cp.company_id
|
||||||
|
WHERE c.name = 'A1 Slovenija' LIMIT 1`).Scan(&planID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("no reference plan found (%v) — enhance may be gated", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := pool.Exec(ctx, `
|
||||||
|
INSERT INTO company_plans (company_id, plan_id, is_active, billing_cycle_start,
|
||||||
|
next_billing_date, total_credits_allocated)
|
||||||
|
VALUES ($1, $2, true, now(), now() + interval '30 days', 100000)`, companyID, planID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = pool.Exec(ctx, `
|
||||||
|
INSERT INTO credit_balances (company_id, total_credits, used_credits)
|
||||||
|
VALUES ($1, 100000, 0)
|
||||||
|
ON CONFLICT (company_id) DO UPDATE SET total_credits = 100000, used_credits = 0`, companyID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCategoryFormulas(cat map[string]any) {
|
||||||
|
for _, k := range []string{"prompts", "title_template", "description_template"} {
|
||||||
|
v, ok := cat[k]
|
||||||
|
if !ok || v == nil {
|
||||||
|
fmt.Printf("%-22s: (none)\n", k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(v)
|
||||||
|
fmt.Printf("%-22s: %s\n", k, trunc(string(b), 420))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectIDs(rows interface {
|
||||||
|
Next() bool
|
||||||
|
Scan(...any) error
|
||||||
|
Err() error
|
||||||
|
Close()
|
||||||
|
}) ([]uuid.UUID, error) {
|
||||||
|
defer rows.Close()
|
||||||
|
var out []uuid.UUID
|
||||||
|
for rows.Next() {
|
||||||
|
var id uuid.UUID
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, id)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func plain(s string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
inTag := false
|
||||||
|
for _, r := range s {
|
||||||
|
switch {
|
||||||
|
case r == '<':
|
||||||
|
inTag = true
|
||||||
|
case r == '>':
|
||||||
|
inTag = false
|
||||||
|
case !inTag:
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(strings.Fields(b.String()), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func trunc(s string, n int) string {
|
||||||
|
s = strings.ReplaceAll(strings.TrimSpace(s), "\n", " ")
|
||||||
|
rs := []rune(s)
|
||||||
|
if len(rs) <= n {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return string(rs[:n]) + "…"
|
||||||
|
}
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Formula-aware local generator.
|
||||||
|
//
|
||||||
|
// processing.HeuristicCompleter is the production offline fallback and stays
|
||||||
|
// deliberately dumb. For local proofs that is not enough: a run only shows the
|
||||||
|
// category formula works if the reply actually follows the formula in the prompt.
|
||||||
|
// This reader parses the product template out of the user message and answers with
|
||||||
|
// copy shaped by it, built ONLY from the supplied name / description / category /
|
||||||
|
// attrs — never invented facts — which is exactly the contract the pipeline gates
|
||||||
|
// the real model against.
|
||||||
|
|
||||||
|
var (
|
||||||
|
reTemplateHeader = regexp.MustCompile(`(?is)(?:GPT predloga|Product template)\s*:\s*(.*?)(?:\n\s*(?:Odgovori SAMO|Reply with ONE)|$)`)
|
||||||
|
reNameBlock = regexp.MustCompile(`(?is)<\s*name\s*>\s*\{?(.*?)\}?\s*<\s*/\s*name\s*>`)
|
||||||
|
reMetaBlock = regexp.MustCompile(`(?is)<\s*metaDescription\s*>\s*\{?(.*?)\}?\s*<\s*/\s*metaDescription\s*>`)
|
||||||
|
reBodyBlock = regexp.MustCompile(`(?is)<\s*(h[1-4]|p|b|ul|li)\s*>\s*\{?(.*?)\}?\s*<\s*/\s*(?:h[1-4]|p|b|ul|li)\s*>`)
|
||||||
|
reAttrsLine = regexp.MustCompile(`(?im)^\s*Attrs:\s*(\{.*\})\s*$`)
|
||||||
|
reQuotedSlot = regexp.MustCompile(`"([^"]+)"`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type formulaBlock struct {
|
||||||
|
tag string
|
||||||
|
text string
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptFacts struct {
|
||||||
|
name string
|
||||||
|
desc string
|
||||||
|
category string
|
||||||
|
attrs map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildFormulaReply returns an enhance JSON reply that satisfies the template in
|
||||||
|
// user, or ok=false when the prompt carries no template (caller falls back).
|
||||||
|
func buildFormulaReply(system, user string) (string, bool) {
|
||||||
|
m := reTemplateHeader.FindStringSubmatch(user)
|
||||||
|
if m == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
tpl := m[1]
|
||||||
|
nameRule := ""
|
||||||
|
if nm := reNameBlock.FindStringSubmatch(tpl); nm != nil {
|
||||||
|
nameRule = strings.TrimSpace(nm[1])
|
||||||
|
}
|
||||||
|
metaRule := ""
|
||||||
|
if mm := reMetaBlock.FindStringSubmatch(tpl); mm != nil {
|
||||||
|
metaRule = strings.TrimSpace(mm[1])
|
||||||
|
}
|
||||||
|
body := reNameBlock.ReplaceAllString(tpl, "")
|
||||||
|
body = reMetaBlock.ReplaceAllString(body, "")
|
||||||
|
var blocks []formulaBlock
|
||||||
|
for _, b := range reBodyBlock.FindAllStringSubmatch(body, -1) {
|
||||||
|
tag := strings.ToLower(strings.TrimSpace(b[1]))
|
||||||
|
blocks = append(blocks, formulaBlock{tag: tag, text: strings.TrimSpace(b[2])})
|
||||||
|
}
|
||||||
|
if nameRule == "" && len(blocks) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
facts := readPromptFacts(user)
|
||||||
|
name := buildFormulaName(nameRule, facts)
|
||||||
|
desc := buildFormulaBody(blocks, name, facts)
|
||||||
|
|
||||||
|
payload := map[string]any{
|
||||||
|
"name": name,
|
||||||
|
"description": desc,
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(system), "meta_title") && metaRule != "" {
|
||||||
|
payload["meta_title"] = truncateRunesLocal(name, 60)
|
||||||
|
payload["meta_description"] = truncateRunesLocal(name+" — "+plainText(desc), 155)
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(system), `"attrs"`) {
|
||||||
|
payload["attrs"] = facts.attrs
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return string(b), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPromptFacts(user string) promptFacts {
|
||||||
|
f := promptFacts{attrs: map[string]any{}}
|
||||||
|
f.name = labeledLine(user, "Staro_ime_izdelka:", "Old_product_name:", "Name:")
|
||||||
|
f.desc = labeledLine(user, "Star_opis_izdelka:", "Old_product_description:", "Description:")
|
||||||
|
f.category = labeledLine(user, "Kategorija:", "Category:")
|
||||||
|
if m := reAttrsLine.FindStringSubmatch(user); m != nil {
|
||||||
|
var attrs map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(m[1]), &attrs); err == nil {
|
||||||
|
f.attrs = attrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func labeledLine(user string, labels ...string) string {
|
||||||
|
for _, line := range strings.Split(user, "\n") {
|
||||||
|
t := strings.TrimSpace(line)
|
||||||
|
for _, label := range labels {
|
||||||
|
if strings.HasPrefix(t, label) {
|
||||||
|
if v := strings.TrimSpace(strings.TrimPrefix(t, label)); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildFormulaName fills the quoted slots of the <name> formula from attrs. Slots
|
||||||
|
// with no matching attribute are dropped rather than guessed.
|
||||||
|
func buildFormulaName(nameRule string, f promptFacts) string {
|
||||||
|
if nameRule == "" {
|
||||||
|
return f.name
|
||||||
|
}
|
||||||
|
var parts []string
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, m := range reQuotedSlot.FindAllStringSubmatch(nameRule, -1) {
|
||||||
|
slot := strings.ToLower(strings.Trim(strings.TrimSpace(m[1]), `",. `))
|
||||||
|
if slot == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v := valueForSlot(slot, f)
|
||||||
|
if v == "" || seen[strings.ToLower(v)] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[strings.ToLower(v)] = true
|
||||||
|
parts = append(parts, v)
|
||||||
|
}
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return f.name
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueForSlot(slot string, f promptFacts) string {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(slot, "znamka") || strings.Contains(slot, "brand"):
|
||||||
|
return attrString(f.attrs, "brand")
|
||||||
|
case strings.Contains(slot, "poln model") || strings.Contains(slot, "product model") ||
|
||||||
|
strings.Contains(slot, "model izdelka") || slot == "model":
|
||||||
|
return attrString(f.attrs, "product_model", "model", "sku")
|
||||||
|
case strings.Contains(slot, "tip izdelka") || strings.Contains(slot, "product type") ||
|
||||||
|
strings.Contains(slot, "vrsta izdelka"):
|
||||||
|
if v := attrString(f.attrs, "product_type"); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return singularCategory(f.category)
|
||||||
|
case strings.Contains(slot, "barva") || strings.Contains(slot, "colour") || strings.Contains(slot, "color"):
|
||||||
|
return attrString(f.attrs, "barva", "color", "colour")
|
||||||
|
case strings.Contains(slot, "procesor") || strings.Contains(slot, "processor"):
|
||||||
|
return attrString(f.attrs, "procesor", "processor", "cpu")
|
||||||
|
case strings.Contains(slot, "pomnilnik") || strings.Contains(slot, "memory"):
|
||||||
|
return attrString(f.attrs, "kapaciteta_ram_pomnilnika", "memory", "ram")
|
||||||
|
case strings.Contains(slot, "kapaciteta") || strings.Contains(slot, "capacity"):
|
||||||
|
return attrString(f.attrs, "kapaciteta", "capacity")
|
||||||
|
case strings.Contains(slot, "dimenzij") || strings.Contains(slot, "dimensions"):
|
||||||
|
return attrString(f.attrs, "dimensions", "dimenzije")
|
||||||
|
case strings.Contains(slot, "diagonal") || strings.Contains(slot, "screen"):
|
||||||
|
return attrString(f.attrs, "diagonala_zaslona", "screen_size")
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func attrString(attrs map[string]any, keys ...string) string {
|
||||||
|
for _, k := range keys {
|
||||||
|
for ak, av := range attrs {
|
||||||
|
if !strings.EqualFold(strings.TrimSpace(ak), k) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s := strings.TrimSpace(fmt.Sprint(av)); s != "" && s != "<nil>" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func singularCategory(cat string) string {
|
||||||
|
c := strings.TrimSpace(cat)
|
||||||
|
if c == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.ToLower(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildFormulaBody emits one HTML element per template block, in order, with the
|
||||||
|
// tag the block asks for. Paragraph text is drawn from the supplied description and
|
||||||
|
// attributes so the reply stays inside the evidence the prompt provided.
|
||||||
|
func buildFormulaBody(blocks []formulaBlock, name string, f promptFacts) string {
|
||||||
|
sentences := splitSentences(f.desc)
|
||||||
|
facts := attrSentences(f.attrs)
|
||||||
|
var b strings.Builder
|
||||||
|
para := 0
|
||||||
|
for _, blk := range blocks {
|
||||||
|
instr := strings.ToLower(blk.text)
|
||||||
|
switch blk.tag {
|
||||||
|
case "h1", "h2", "h3", "h4":
|
||||||
|
heading := name
|
||||||
|
if strings.Contains(instr, "ne napiši") || strings.Contains(instr, "do not write") ||
|
||||||
|
strings.Contains(instr, "brez") || strings.Contains(instr, "without") {
|
||||||
|
heading = benefitHeading(f)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "<%s>%s</%s>", blk.tag, escapeHTML(heading), blk.tag)
|
||||||
|
case "ul", "li":
|
||||||
|
items := facts
|
||||||
|
if len(items) == 0 {
|
||||||
|
items = []string{"Category: " + f.category}
|
||||||
|
}
|
||||||
|
b.WriteString("<ul>")
|
||||||
|
for _, it := range items {
|
||||||
|
fmt.Fprintf(&b, "<li>%s</li>", escapeHTML(it))
|
||||||
|
}
|
||||||
|
b.WriteString("</ul>")
|
||||||
|
case "b":
|
||||||
|
fmt.Fprintf(&b, "<b>%s</b>", escapeHTML(strings.TrimSpace(blk.text)))
|
||||||
|
default: // p
|
||||||
|
includeName := strings.Contains(instr, "vključi") || strings.Contains(instr, "does include") ||
|
||||||
|
strings.Contains(instr, "include the new product name")
|
||||||
|
excludeName := strings.Contains(instr, "ne vsebuje") || strings.Contains(instr, "not contain") ||
|
||||||
|
strings.Contains(instr, "not include")
|
||||||
|
text := paragraphFor(sentences, para, f)
|
||||||
|
if includeName && !excludeName {
|
||||||
|
text = name + " — " + text
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "<p>%s</p>", escapeHTML(text))
|
||||||
|
para++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func paragraphFor(sentences []string, idx int, f promptFacts) string {
|
||||||
|
if len(sentences) == 0 {
|
||||||
|
if c := strings.TrimSpace(f.category); c != "" {
|
||||||
|
return "Listed under " + c + " with the details supplied by the supplier feed."
|
||||||
|
}
|
||||||
|
return "Details as supplied by the supplier feed."
|
||||||
|
}
|
||||||
|
// Rotate through the supplied sentences so repeated paragraphs differ, then pad
|
||||||
|
// with attribute facts to a body length the quality gate accepts.
|
||||||
|
var parts []string
|
||||||
|
for i := 0; i < len(sentences); i++ {
|
||||||
|
parts = append(parts, sentences[(idx+i)%len(sentences)])
|
||||||
|
}
|
||||||
|
out := strings.Join(parts, " ")
|
||||||
|
for _, fact := range attrSentences(f.attrs) {
|
||||||
|
if len([]rune(out)) >= 220 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
out += " " + fact + "."
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func benefitHeading(f promptFacts) string {
|
||||||
|
if c := strings.TrimSpace(f.category); c != "" {
|
||||||
|
return "Built for everyday " + strings.ToLower(c)
|
||||||
|
}
|
||||||
|
return "Built for everyday use"
|
||||||
|
}
|
||||||
|
|
||||||
|
func attrSentences(attrs map[string]any) []string {
|
||||||
|
keys := make([]string, 0, len(attrs))
|
||||||
|
for k := range attrs {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
out := make([]string, 0, len(keys))
|
||||||
|
for _, k := range keys {
|
||||||
|
v := strings.TrimSpace(fmt.Sprint(attrs[k]))
|
||||||
|
if v == "" || v == "<nil>" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, prettyKey(k)+": "+v)
|
||||||
|
if len(out) >= 8 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func prettyKey(k string) string {
|
||||||
|
k = strings.ReplaceAll(strings.TrimSpace(k), "_", " ")
|
||||||
|
if k == "" {
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
return strings.ToUpper(k[:1]) + k[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitSentences(s string) []string {
|
||||||
|
s = strings.TrimSpace(plainText(s))
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
for _, part := range strings.FieldsFunc(s, func(r rune) bool { return r == '.' || r == '!' || r == '?' }) {
|
||||||
|
p := strings.TrimSpace(part)
|
||||||
|
if len([]rune(p)) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, p+".")
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func plainText(s string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
inTag := false
|
||||||
|
for _, r := range s {
|
||||||
|
switch {
|
||||||
|
case r == '<':
|
||||||
|
inTag = true
|
||||||
|
case r == '>':
|
||||||
|
inTag = false
|
||||||
|
case !inTag:
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(strings.Fields(b.String()), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeHTML(s string) string {
|
||||||
|
r := strings.NewReplacer("<", "", ">", "", "&", "and")
|
||||||
|
return strings.TrimSpace(r.Replace(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateRunesLocal(s string, n int) string {
|
||||||
|
rs := []rune(strings.TrimSpace(s))
|
||||||
|
if len(rs) <= n {
|
||||||
|
return string(rs)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(rs[:n]))
|
||||||
|
}
|
||||||
@@ -140,10 +140,19 @@ func (s *server) handleChatCompletions(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
system, user := splitMessages(req.Messages)
|
system, user := splitMessages(req.Messages)
|
||||||
comp, err := processing.HeuristicCompleter{}.Complete(context.Background(), system, user)
|
var comp processing.Completion
|
||||||
if err != nil {
|
// A category formula in the prompt is answered in that formula's shape, so a
|
||||||
http.Error(w, `{"error":{"message":"completer failed"}}`, http.StatusInternalServerError)
|
// local run proves the formula reached the model and the reply passes the
|
||||||
return
|
// pipeline's formula gate. Everything else keeps the heuristic fallback.
|
||||||
|
if text, ok := buildFormulaReply(system, user); ok {
|
||||||
|
comp = processing.Completion{Text: text}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
comp, err = processing.HeuristicCompleter{}.Complete(context.Background(), system, user)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, `{"error":{"message":"completer failed"}}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
model := strings.TrimSpace(req.Model)
|
model := strings.TrimSpace(req.Model)
|
||||||
if model == "" {
|
if model == "" {
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package aiprompts
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Platform-default category formulas.
|
||||||
|
//
|
||||||
|
// The A1 tenant keeps its Slovenian legacy prompts (scripts/seed/a1-category-prompts.json,
|
||||||
|
// applied by cmd/seed-a1). Every other tenant gets the same formula STYLE in
|
||||||
|
// English: a <name> naming formula, a <metaDescription> rule, and an HTML body of
|
||||||
|
// heading/paragraph pairs plus a technical specification list.
|
||||||
|
//
|
||||||
|
// defaults/default-category-prompts.json is generated from the A1 seed by
|
||||||
|
// scripts/seed/build-default-category-prompts.mjs and embedded so the defaults ship
|
||||||
|
// with the binary. Categories outside that taxonomy fall back to
|
||||||
|
// GenericCategoryFormula, which carries the same structure without category-specific
|
||||||
|
// naming slots.
|
||||||
|
|
||||||
|
//go:embed defaults/default-category-prompts.json
|
||||||
|
var defaultCategoryPromptsJSON []byte
|
||||||
|
|
||||||
|
// GenericDefaultCategoryPrompt is the English formula applied to any category the
|
||||||
|
// seed does not name. Same shape as the A1 template: naming formula, meta rule, two
|
||||||
|
// heading/paragraph pairs, technical specifications list.
|
||||||
|
const GenericDefaultCategoryPrompt = `Create a new product description in {{language}} using the following variables:
|
||||||
|
|
||||||
|
Old_product_description: {"PRODUCT DESCRIPTION"};
|
||||||
|
Old_product_name: {"OLD PRODUCT NAME"};
|
||||||
|
|
||||||
|
Use the product template below.
|
||||||
|
|
||||||
|
Follow this product template sentence by sentence and compose a new product description:
|
||||||
|
|
||||||
|
Product template:
|
||||||
|
|
||||||
|
<name>{Write a new product name using this formula: "product type in sentence case", "brand with correct capitalisation", "full product model, with the word and the ID in uppercase when available". Do not use commas.}</name>
|
||||||
|
<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>
|
||||||
|
|
||||||
|
<H2>{Write the New product name and highlight one benefit}</H2>
|
||||||
|
<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>
|
||||||
|
<H2>{Highlight one benefit and do NOT write the New product name}</H2>
|
||||||
|
<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>`
|
||||||
|
|
||||||
|
// IsBuiltInProductEnhanceUserTemplate reports whether a resolved enhance user
|
||||||
|
// template is the shipped default rather than tenant-authored copy. Service.Resolve
|
||||||
|
// substitutes the built-in when a company has no custom row, so emptiness alone
|
||||||
|
// cannot tell the two apart — and platform defaults must never override a template
|
||||||
|
// a tenant actually wrote.
|
||||||
|
func IsBuiltInProductEnhanceUserTemplate(tpl string) bool {
|
||||||
|
t := strings.TrimSpace(tpl)
|
||||||
|
return t == "" || t == strings.TrimSpace(CategoryEnhanceUserTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryFormulaDefaults is a resolved platform default for one category: the
|
||||||
|
// role-sectioned enhance overlay plus the structured formulas derived from it.
|
||||||
|
// Empty TitleTemplateJSON / DescriptionTemplateJSON mean "nothing derivable".
|
||||||
|
type CategoryFormulaDefaults struct {
|
||||||
|
// MatchedName is the seed entry that matched, empty for the generic fallback.
|
||||||
|
MatchedName string
|
||||||
|
// Overlay is the role-sectioned text for categories.prompt.
|
||||||
|
Overlay string
|
||||||
|
// TitleTemplateJSON / DescriptionTemplateJSON are for categories.title_template
|
||||||
|
// and categories.description_template.
|
||||||
|
TitleTemplateJSON string
|
||||||
|
DescriptionTemplateJSON string
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultCategoryPromptEntry struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Aliases []string `json:"aliases"`
|
||||||
|
UniqueID string `json:"unique_id"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultCategoryPromptsFile struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
Entries []defaultCategoryPromptEntry `json:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultFormulaOnce sync.Once
|
||||||
|
defaultFormulaByKey map[string]CategoryFormulaDefaults
|
||||||
|
defaultFormulaGeneric CategoryFormulaDefaults
|
||||||
|
defaultFormulaCount int
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadDefaultCategoryFormulas() {
|
||||||
|
defaultFormulaGeneric = buildCategoryFormulaDefaults("", GenericDefaultCategoryPrompt)
|
||||||
|
defaultFormulaByKey = map[string]CategoryFormulaDefaults{}
|
||||||
|
|
||||||
|
var f defaultCategoryPromptsFile
|
||||||
|
if err := json.Unmarshal(defaultCategoryPromptsJSON, &f); err != nil {
|
||||||
|
// Embedded asset is generated and checked in CI; a parse failure must not
|
||||||
|
// take the process down — the generic default still applies.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, e := range f.Entries {
|
||||||
|
prompt := strings.TrimSpace(e.Prompt)
|
||||||
|
if prompt == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res := buildCategoryFormulaDefaults(strings.TrimSpace(e.Name), prompt)
|
||||||
|
if res.Overlay == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defaultFormulaCount++
|
||||||
|
for _, key := range append([]string{e.Name, e.UniqueID}, e.Aliases...) {
|
||||||
|
if k := NormalizeCategoryKey(key); k != "" {
|
||||||
|
defaultFormulaByKey[k] = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCategoryFormulaDefaults(name, legacyPrompt string) CategoryFormulaDefaults {
|
||||||
|
parts := ParseLegacyCombinedEnhancePrompt(legacyPrompt)
|
||||||
|
if !parts.WasLegacy {
|
||||||
|
return CategoryFormulaDefaults{}
|
||||||
|
}
|
||||||
|
out := CategoryFormulaDefaults{
|
||||||
|
MatchedName: name,
|
||||||
|
Overlay: BuildCategoryEnhanceOverlay(parts),
|
||||||
|
}
|
||||||
|
desc := DeriveDescriptionFormulaFromLegacyParts(parts)
|
||||||
|
// The derivation seeds a Slovenian meta-title rule for legacy A1 dumps; these
|
||||||
|
// formulas are the English set, so the whole formula must read in English.
|
||||||
|
if strings.TrimSpace(desc.MetaTitle) != "" {
|
||||||
|
desc.MetaTitle = DefaultMetaTitleRuleEN
|
||||||
|
}
|
||||||
|
if len(desc.Sections) > 0 || desc.MetaTitle != "" || desc.MetaDescription != "" {
|
||||||
|
if b, err := json.Marshal(desc); err == nil {
|
||||||
|
out.DescriptionTemplateJSON = string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(parts.TitleRules) != "" {
|
||||||
|
out.TitleTemplateJSON = DeriveTitleTemplateJSON(parts.TitleRules)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultCategoryFormula returns the platform-default formulas for a category,
|
||||||
|
// matched on unique_id or display name (case / diacritic / spacing tolerant, and
|
||||||
|
// aware of the original Slovenian taxonomy names). Unmatched categories get the
|
||||||
|
// generic English default, so the result is always usable.
|
||||||
|
func DefaultCategoryFormula(uniqueID, name string) CategoryFormulaDefaults {
|
||||||
|
defaultFormulaOnce.Do(loadDefaultCategoryFormulas)
|
||||||
|
for _, key := range []string{uniqueID, name} {
|
||||||
|
k := NormalizeCategoryKey(key)
|
||||||
|
if k == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res, ok := defaultFormulaByKey[k]; ok {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultFormulaGeneric
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenericCategoryFormula is DefaultCategoryFormula's fallback on its own — used
|
||||||
|
// when only the shape matters and the category is unknown.
|
||||||
|
func GenericCategoryFormula() CategoryFormulaDefaults {
|
||||||
|
defaultFormulaOnce.Do(loadDefaultCategoryFormulas)
|
||||||
|
return defaultFormulaGeneric
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultCategoryFormulaCount is the number of seeded category entries (test/diagnostics).
|
||||||
|
func DefaultCategoryFormulaCount() int {
|
||||||
|
defaultFormulaOnce.Do(loadDefaultCategoryFormulas)
|
||||||
|
return defaultFormulaCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeCategoryKey folds a category name or unique_id for matching:
|
||||||
|
// lowercase, Slovenian diacritics folded, punctuation dropped, spaces collapsed.
|
||||||
|
func NormalizeCategoryKey(s string) string {
|
||||||
|
s = strings.ToLower(strings.TrimSpace(s))
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(s))
|
||||||
|
prevSpace := false
|
||||||
|
for _, r := range s {
|
||||||
|
switch r {
|
||||||
|
case 'č', 'ć':
|
||||||
|
r = 'c'
|
||||||
|
case 'š':
|
||||||
|
r = 's'
|
||||||
|
case 'ž':
|
||||||
|
r = 'z'
|
||||||
|
case 'đ':
|
||||||
|
r = 'd'
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case unicode.IsLetter(r) || unicode.IsDigit(r):
|
||||||
|
b.WriteRune(r)
|
||||||
|
prevSpace = false
|
||||||
|
case unicode.IsSpace(r) || r == '-' || r == '_' || r == '/':
|
||||||
|
if !prevSpace && b.Len() > 0 {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
prevSpace = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(b.String())
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package aiprompts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The platform defaults are the A1 formula style translated to English: a naming
|
||||||
|
// formula, a meta rule, and heading/paragraph pairs closing with a technical
|
||||||
|
// specification list.
|
||||||
|
func TestDefaultCategoryFormula_matchesA1Shape(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
def := DefaultCategoryFormula("", "High chairs")
|
||||||
|
if def.MatchedName != "High chairs" {
|
||||||
|
t.Fatalf("MatchedName=%q want the seeded entry", def.MatchedName)
|
||||||
|
}
|
||||||
|
for _, want := range []string{SectionTitleStart, SectionDescriptionStart, SectionMetaStart} {
|
||||||
|
if !strings.Contains(def.Overlay, want) {
|
||||||
|
t.Fatalf("overlay missing %q:\n%s", want, def.Overlay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
title, ok := parseDescriptionFormulaOK(def.TitleTemplateJSON)
|
||||||
|
_ = title
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("title template is not JSON: %s", def.TitleTemplateJSON)
|
||||||
|
}
|
||||||
|
els, ok := titleFormulaElements(def.TitleTemplateJSON)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("title template has no elements: %s", def.TitleTemplateJSON)
|
||||||
|
}
|
||||||
|
var vars []string
|
||||||
|
for _, el := range els {
|
||||||
|
if el.Type == "variable" {
|
||||||
|
vars = append(vars, el.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mirrors the Slovenian Stolčki formula: znamka, tip izdelka, poln model.
|
||||||
|
want := []string{"brand", "product_type", "product_model"}
|
||||||
|
if strings.Join(vars, ",") != strings.Join(want, ",") {
|
||||||
|
t.Fatalf("title slots = %v want %v", vars, want)
|
||||||
|
}
|
||||||
|
if TitleTemplateIsBrandOnly(def.TitleTemplateJSON) {
|
||||||
|
t.Fatal("default title formula must never be a brand-only stub")
|
||||||
|
}
|
||||||
|
|
||||||
|
f, _ := parseDescriptionFormula(def.DescriptionTemplateJSON)
|
||||||
|
var types []string
|
||||||
|
for _, s := range f.Sections {
|
||||||
|
types = append(types, s.Type)
|
||||||
|
}
|
||||||
|
if strings.Join(types, ",") != "h2,p,h2,p,ul" {
|
||||||
|
t.Fatalf("description sections = %v want h2,p,h2,p,ul", types)
|
||||||
|
}
|
||||||
|
if f.MetaTitle == "" || f.MetaDescription == "" {
|
||||||
|
t.Fatalf("meta rules missing: %+v", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDescriptionFormulaOK(s string) (DescriptionFormula, bool) {
|
||||||
|
return parseDescriptionFormula(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category matching must survive the original Slovenian taxonomy names and their
|
||||||
|
// diacritics, so an imported A1-style taxonomy still resolves to a default.
|
||||||
|
func TestDefaultCategoryFormula_matchesAliasesAndFolding(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
want := DefaultCategoryFormula("", "High chairs").Overlay
|
||||||
|
for _, key := range []string{"Stolčki", "stolcki", " STOLČKI ", "High Chairs"} {
|
||||||
|
if got := DefaultCategoryFormula("", key).Overlay; got != want {
|
||||||
|
t.Fatalf("key %q did not resolve to the High chairs default", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anything outside the seeded taxonomy still gets a usable formula.
|
||||||
|
func TestDefaultCategoryFormula_genericFallback(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
def := DefaultCategoryFormula("", "Artisan Sourdough Starters")
|
||||||
|
if def.MatchedName != "" {
|
||||||
|
t.Fatalf("unmapped category should use the generic default, matched %q", def.MatchedName)
|
||||||
|
}
|
||||||
|
if !strings.Contains(def.Overlay, SectionDescriptionStart) || def.TitleTemplateJSON == "" {
|
||||||
|
t.Fatalf("generic default is not usable: %+v", def)
|
||||||
|
}
|
||||||
|
f, _ := parseDescriptionFormula(def.DescriptionTemplateJSON)
|
||||||
|
if len(f.Sections) == 0 {
|
||||||
|
t.Fatal("generic default has no description sections")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The whole point of the English set is that it reads in English. A leftover
|
||||||
|
// Slovenian phrase would ship a mixed-language formula to every new tenant.
|
||||||
|
func TestDefaultCategoryFormulas_areEnglish(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
slovene := regexp.MustCompile(`(?i)[čšž]|\b(napi[sš]i|izdelka|izpostavi|odstavek|znamka|besed|predloga)\b`)
|
||||||
|
if n := DefaultCategoryFormulaCount(); n < 100 {
|
||||||
|
t.Fatalf("expected the full translated seed, got %d entries", n)
|
||||||
|
}
|
||||||
|
for _, name := range []string{"High chairs", "Televisions", "Cables", "Washing machines", "Laptops"} {
|
||||||
|
def := DefaultCategoryFormula("", name)
|
||||||
|
if def.MatchedName == "" {
|
||||||
|
t.Fatalf("%q is missing from the default seed", name)
|
||||||
|
}
|
||||||
|
for label, text := range map[string]string{
|
||||||
|
"overlay": def.Overlay,
|
||||||
|
"title": def.TitleTemplateJSON,
|
||||||
|
"description": def.DescriptionTemplateJSON,
|
||||||
|
} {
|
||||||
|
if m := slovene.FindString(text); m != "" {
|
||||||
|
t.Fatalf("%s %s still contains Slovenian %q:\n%s", name, label, m, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m := slovene.FindString(GenericDefaultCategoryPrompt); m != "" {
|
||||||
|
t.Fatalf("generic default contains Slovenian %q", m)
|
||||||
|
}
|
||||||
|
if m := slovene.FindString(DefaultMetaTitleRuleEN); m != "" {
|
||||||
|
t.Fatalf("English meta title rule contains Slovenian %q", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A1 keeps its Slovenian formulas: the English seed must not be reachable from the
|
||||||
|
// legacy combined prompt path that seed-a1 uses.
|
||||||
|
func TestSplitLegacyCombinedEnhancePrompt_handlesBothScaffoldings(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
en := GenericDefaultCategoryPrompt
|
||||||
|
if !IsLegacyCombinedEnhancePrompt(en) {
|
||||||
|
t.Fatal("English default must parse as a combined formula prompt")
|
||||||
|
}
|
||||||
|
overlay := SplitLegacyCombinedEnhancePrompt(en)
|
||||||
|
if !strings.Contains(overlay, SectionTitleStart) || !strings.Contains(overlay, "<H2>") {
|
||||||
|
t.Fatalf("English split lost its sections:\n%s", overlay)
|
||||||
|
}
|
||||||
|
// Scaffolding lines must not leak into the Description role.
|
||||||
|
body := ExtractEnhanceSectionBody(overlay, SectionDescriptionStart, SectionDescriptionEnd)
|
||||||
|
for _, leak := range []string{"Create a new product description", "Old_product_name", "Product template:"} {
|
||||||
|
if strings.Contains(body, leak) {
|
||||||
|
t.Fatalf("scaffolding %q leaked into the description formula:\n%s", leak, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsBuiltInProductEnhanceUserTemplate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if !IsBuiltInProductEnhanceUserTemplate("") {
|
||||||
|
t.Fatal("empty is built-in")
|
||||||
|
}
|
||||||
|
if !IsBuiltInProductEnhanceUserTemplate(CategoryEnhanceUserTemplate) {
|
||||||
|
t.Fatal("the shipped template is built-in")
|
||||||
|
}
|
||||||
|
if IsBuiltInProductEnhanceUserTemplate("Write a haiku about {{name}}") {
|
||||||
|
t.Fatal("tenant copy must not read as built-in")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,820 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"source": "a1-category-prompts.json (translated by scripts/seed/build-default-category-prompts.mjs)",
|
||||||
|
"language": "en",
|
||||||
|
"note": "English platform-default category formulas. A1 Slovenija keeps a1-category-prompts.json.",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"name": "Additional equipment",
|
||||||
|
"aliases": [
|
||||||
|
"Dodatna oprema"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Air conditioners",
|
||||||
|
"aliases": [
|
||||||
|
"Klimatske naprave"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\r\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\r\n\r\nUse the product template below. \r\n\r\nFollow this product template sentence by sentence and compose a new product description:\r\n\r\nProduct template:\r\n\r\n\r\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"air conditioner with installation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n\r\n<H2>{Write the New product name and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\r\n<H2>{Write the New product name and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Air fryers",
|
||||||
|
"aliases": [
|
||||||
|
"Cvrtniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"PRODUCT DESCRIPTION\"};\r\nOld_product_name: {\"OLD PRODUCT NAME\"};\r\n\r\nUse the product template below.\r\n\r\nProduct template:\r\n\r\n<name>{Write a new product name using this formula: \"brand with correct capitalisation\", \"air fryer in lowercase\", \"product model in uppercase\", \"capacity in litres\". Do not use commas.}</name>\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n\r\n<H2>{Write the New product name without the capacity, and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name without the capacity}</p>\r\n<H2>{Write the New product name without the capacity, and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name without the capacity}</p>\r\n<H2>{Write the New product name without the capacity, and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n\r\n<b>Technical specifications</b>\r\n<ul>\r\n<li>{Write 3-10 technical specifications as bullet points}</li>\r\n</ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Balance bikes",
|
||||||
|
"aliases": [
|
||||||
|
"Poganjalčki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ball sports",
|
||||||
|
"aliases": [
|
||||||
|
"Športi z žogo"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Beds",
|
||||||
|
"aliases": [
|
||||||
|
"Postelje"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"PRODUCT DESCRIPTION\"};\r\nOld_product_name: {\"OLD PRODUCT NAME\"};\r\n\r\nUse the product template below.\r\n\r\nFollow this product template sentence by sentence and compose a new product description:\r\n\r\nProduct template:\r\n<name>{Write a new product name using this formula: \"\"bed\"\", \"brand with correct capitalisation\", \"product model\", \"size in cm (e.g. 120x200cm)\". Do not use commas.}</name>\r\n\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n<H2>{Write the bed material (e.g. \"Wooden bed\" or \"Metal bed\"), highlight one benefit and DO include the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\r\n<b>Technical specifications</b>\r\n<ul>\r\n<li>{Write 3-10 technical specifications as bullet points}</li>\r\n</ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bluetooth speakers",
|
||||||
|
"aliases": [
|
||||||
|
"Bluetooth zvočniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"colour as a single word\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Board games",
|
||||||
|
"aliases": [
|
||||||
|
"Družabne igre"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"game title\"\", \"\"model\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cables",
|
||||||
|
"aliases": [
|
||||||
|
"Kabli"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"PRODUCT DESCRIPTION\"};\r\nOld_product_name: {\"OLD PRODUCT NAME\"};\r\n\r\nUse the product template below. \r\n\r\nFollow this product template sentence by sentence and compose a new product description:\r\n\r\nProduct template:\r\n\r\n\r\n<name>{Write a new product name using this formula: \"cable type (UTP/STP…)\", \"cable\", \"length [m]\", \"brand in normal case\", \"product model in uppercase\". If it is a charging cable, build the name like this: \"charging cable\", \"brand in normal case\", \"product model in uppercase\". Do not use commas.}</name>\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n<H1>{Write the New product name}</H1>\r\n<H2>{Include the New product name and highlight one benefit}</H2>\r\n<p>{Continue from the heading and write 100 words about that benefit, without including the New product name}</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cameras",
|
||||||
|
"aliases": [
|
||||||
|
"Kamere in fotoaparati"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Camping and outdoor equipment",
|
||||||
|
"aliases": [
|
||||||
|
"Kamp oprema in aktivnosti na prostem"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"PRODUCT DESCRIPTION\"};\r\nOld_product_name: {\"OLD PRODUCT NAME\"};\r\n\r\nUse the product template below.\r\n\r\nFollow this product template sentence by sentence and compose a new product description:\r\n\r\nProduct template:\r\n\r\n<name>{Write a new product name using this formula: \"product type in lowercase\", \"brand with correct capitalisation\", \"product model\". Do not use commas.}</name>\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n\r\n<H2>{Write the New product name and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name}</p>\r\n<H2>{Write the New product name and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name}</p>\r\n<H2>{Write the New product name and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n\r\n<b>Technical specifications</b>\r\n<ul>\r\n<li>{Write 3-10 technical specifications as bullet points}</li>\r\n</ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Car accessories",
|
||||||
|
"aliases": [
|
||||||
|
"Avto oprema"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Car chargers",
|
||||||
|
"aliases": [
|
||||||
|
"Avto polnilniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"connector type (e.g. USB C)\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Car seats and infant carriers",
|
||||||
|
"aliases": [
|
||||||
|
"Avtosedeži in lupinice"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"weight group [kg]\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the weight group, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the total weight}</p>\n<H2>{Write the New product name without the total weight, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the total weight}</p>\n<H2>{Write the New product name without the total weight, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cases and folios",
|
||||||
|
"aliases": [
|
||||||
|
"Ovitki in mape"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"model of the device it is intended for\"\", colour v eni besedi\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the product type in lowercase}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Classic bikes",
|
||||||
|
"aliases": [
|
||||||
|
"Klasična kolesa"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Coffee machines",
|
||||||
|
"aliases": [
|
||||||
|
"Kavni aparati"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Computer accessories",
|
||||||
|
"aliases": [
|
||||||
|
"Dodatna oprema za računalništvo"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Computer furniture",
|
||||||
|
"aliases": [
|
||||||
|
"Računalniško pohištvo"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Consumables",
|
||||||
|
"aliases": [
|
||||||
|
"Potrošni material"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"PRODUCT DESCRIPTION\"};\r\nOld_product_name: {\"OLD PRODUCT NAME\"};\r\n\r\nUse the product template below.\r\n\r\nFollow this product template sentence by sentence and compose a new product description:\r\n\r\nProduct template:\r\n<name>{Write a new product name using this formula: \"product type in lowercase\", \"brand with correct capitalisation\", \"product model in uppercase\". Do not use commas.}</name>\r\n\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n<H2>{Highlight one benefit and DO include the New product name}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and DO include the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\r\n<b>Technical specifications</b>\r\n<ul>\r\n<li>{Write 3-10 technical specifications as bullet points}</li>\r\n</ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cooker hoods",
|
||||||
|
"aliases": [
|
||||||
|
"Kuhinjske nape"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"width [cm]\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the width, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the width}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cookers",
|
||||||
|
"aliases": [
|
||||||
|
"Štedilniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cookware",
|
||||||
|
"aliases": [
|
||||||
|
"Posode za kuhanje"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Desktop computers",
|
||||||
|
"aliases": [
|
||||||
|
"Namizni računalniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"processor type\"\", \"\"graphics card type\"\", \"\"memory\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the processor, without the graphics card and without the memory, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the processor, without the graphics card and without the memory}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Digital pens",
|
||||||
|
"aliases": [
|
||||||
|
"Digitalna peresa"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dishwashers",
|
||||||
|
"aliases": [
|
||||||
|
"Pomivalni stroji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"width [cm]\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the width, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the width}</p>\n<H2>{Write the New product name without the width, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the width}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the width}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Drones",
|
||||||
|
"aliases": [
|
||||||
|
"Droni"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "E-readers",
|
||||||
|
"aliases": [
|
||||||
|
"Žepni bralniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Electric bikes",
|
||||||
|
"aliases": [
|
||||||
|
"Električna kolesa"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Electric scooters",
|
||||||
|
"aliases": [
|
||||||
|
"Električni skiroji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Electric toothbrushes",
|
||||||
|
"aliases": [
|
||||||
|
"Električne zobne ščetke"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Epilators",
|
||||||
|
"aliases": [
|
||||||
|
"Depilatorji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fitness equipment",
|
||||||
|
"aliases": [
|
||||||
|
"Fitnes oprema"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Food processors",
|
||||||
|
"aliases": [
|
||||||
|
"Kuhinjski roboti"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Freezers",
|
||||||
|
"aliases": [
|
||||||
|
"Zamrzovalniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Game consoles and games",
|
||||||
|
"aliases": [
|
||||||
|
"Igralne konzole in igre"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gaming desktops",
|
||||||
|
"aliases": [
|
||||||
|
"Gaming stacionarni računalniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"processor\"\", \"\"graphics card\"\", \"\"memory\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the product type in sentence case and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the product type in lowercase}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gaming furniture",
|
||||||
|
"aliases": [
|
||||||
|
"Gaming pohištvo"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gaming laptops",
|
||||||
|
"aliases": [
|
||||||
|
"Gaming prenosniki računalniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"processor\"\", \"\"graphics card\"\", \"\"memory\"\", \"\"dimensions\"\", \"\"refresh rate (when it is 144Hz)\"\", \"\"SD / HD / FHD / UHD / 4K / 8K\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the product type in sentence case and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the product type in lowercase}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gaming monitors",
|
||||||
|
"aliases": [
|
||||||
|
"Gaming monitorji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"SD / HD / Full HD / 2K / 4K / 8K\"\", \"\"monitor in lowercase\"\", \"\"Refresh rate in Hz\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"screen diagonal in inches marked with '.\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the screen diagonal, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name without the screen diagonal, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Garden tools",
|
||||||
|
"aliases": [
|
||||||
|
"Vrtno orodje"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Generators",
|
||||||
|
"aliases": [
|
||||||
|
"Agregati"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Grills",
|
||||||
|
"aliases": [
|
||||||
|
"Žari"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Guillotines and trimmers",
|
||||||
|
"aliases": [
|
||||||
|
"Giljotine in rezalniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"PRODUCT DESCRIPTION\"};\r\nOld_product_name: {\"OLD PRODUCT NAME\"};\r\n\r\nUse the product template below.\r\n\r\nFollow this product template sentence by sentence and compose a new product description:\r\n\r\nProduct template:\r\n<name>{Write a new product name using this formula: \"product type (\"Guillotine\" or \"Trimmer\")\", \"brand with correct capitalisation\", \"product model in uppercase\". Do not use commas.}</name>\r\n\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n<H2>{Highlight one benefit and DO include the New product name}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and DO include the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\r\n<b>Technical specifications</b>\r\n<ul>\r\n<li>{Write 3-10 technical specifications as bullet points}</li>\r\n</ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hair clippers",
|
||||||
|
"aliases": [
|
||||||
|
"Strižniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hair stylers and dryers",
|
||||||
|
"aliases": [
|
||||||
|
"Oblikovalci in sušilniki za lase"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Headphones",
|
||||||
|
"aliases": [
|
||||||
|
"Slušalke"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Heaters",
|
||||||
|
"aliases": [
|
||||||
|
"Naprave za ogrevanje"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "High chairs",
|
||||||
|
"aliases": [
|
||||||
|
"Stolčki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hobs",
|
||||||
|
"aliases": [
|
||||||
|
"Kuhalne plošče"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Home accessories",
|
||||||
|
"aliases": [
|
||||||
|
"Dodatki za dom"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\r\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\r\n\r\nUse the product template below. \r\n\r\nFollow this product template sentence by sentence and compose a new product description:\r\n\r\nProduct template:\r\n\r\n\r\n<name>{Write a new product name using this formula: \"\"product type in lowercase\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n\r\n<H2>{Write the New product name and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that does NOT include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hot tubs",
|
||||||
|
"aliases": [
|
||||||
|
"Masažni bazeni"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Humidifiers and air purifiers",
|
||||||
|
"aliases": [
|
||||||
|
"Vlažilniki in čistilci zraka"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Irons",
|
||||||
|
"aliases": [
|
||||||
|
"Likalniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Keyboards",
|
||||||
|
"aliases": [
|
||||||
|
"Tipkovnice"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kids smartwatches",
|
||||||
|
"aliases": [
|
||||||
|
"Otroške pametne ure"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"PRODUCT DESCRIPTION\"};\r\nOld_product_name: {\"OLD PRODUCT NAME\"};\r\n\r\nUse the product template below.\r\n\r\nProduct template:\r\n\r\n<name>{Write a new product name using this formula: \"brand with correct capitalisation\", \"kids smartwatch in lowercase\", \"product model in uppercase\", \"colour\". Do not use commas.}</name>\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n\r\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour}</p>\r\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour}</p>\r\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n\r\n<b>Technical specifications</b>\r\n<ul>\r\n<li>{Write 3-10 technical specifications as bullet points}</li>\r\n</ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Label and tape printers",
|
||||||
|
"aliases": [
|
||||||
|
"Tiskalniki nalepk in trakov"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"PRODUCT DESCRIPTION\"};\r\nOld_product_name: {\"OLD PRODUCT NAME\"};\r\n\r\nUse the product template below.\r\n\r\nFollow this product template sentence by sentence and compose a new product description:\r\n\r\nProduct template:\r\n<name>{Write a new product name using this formula: \"\"Label printer\"\", \"brand with correct capitalisation\", \"product model in uppercase\". Do not use commas.}</name>\r\n\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n<H2>{Highlight one benefit and DO include the New product name}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and DO include the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\r\n<b>Technical specifications</b>\r\n<ul>\r\n<li>{Write 3-10 technical specifications as bullet points}</li>\r\n</ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Laminators",
|
||||||
|
"aliases": [
|
||||||
|
"Plastifikatorji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"PRODUCT DESCRIPTION\"};\r\nOld_product_name: {\"OLD PRODUCT NAME\"};\r\n\r\nUse the product template below.\r\n\r\nFollow this product template sentence by sentence and compose a new product description:\r\n\r\nProduct template:\r\n<name>{Write a new product name using this formula: \"\"Laminator\"\", \"brand with correct capitalisation\", \"product model in uppercase\". Do not use commas.}</name>\r\n\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n<H2>{Highlight one benefit and DO include the New product name}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and DO include the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\r\n<b>Technical specifications</b>\r\n<ul>\r\n<li>{Write 3-10 technical specifications as bullet points}</li>\r\n</ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Laptop cases and bags",
|
||||||
|
"aliases": [
|
||||||
|
"Ovitki in torbe za prenosnike"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"dimensions\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the dimensions, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Laptops",
|
||||||
|
"aliases": [
|
||||||
|
"Prenosni računalniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\r\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\r\n\r\nUse the product template below. \r\n\r\nFollow this product template sentence by sentence and compose a new product description:\r\n\r\nProduct template:\r\n\r\n\r\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"processor\"\", \"\"graphics card\"\", \"\"memory\"\", \"\"screen size in inches - (example 13.6'')\"\", \"\"refresh rate (when it is 144Hz)\"\", \"\"SD / HD / FHD / UHD / 4K / 8K\"\". Do not use commas.}</name>\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n\r\n<H2>{Write the product type in sentence case and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the product type in lowercase}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mice",
|
||||||
|
"aliases": [
|
||||||
|
"Miške"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Microwave ovens",
|
||||||
|
"aliases": [
|
||||||
|
"Mikrovalovne pečice"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mixers and blenders",
|
||||||
|
"aliases": [
|
||||||
|
"Mešalniki in blenderji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mobile devices",
|
||||||
|
"aliases": [
|
||||||
|
"Mobilne naprave"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"memory size\"\", \"\"colour as a single word\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour}</p>\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Monitors",
|
||||||
|
"aliases": [
|
||||||
|
"Monitorji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"SD / HD / Full HD / 2K / 4K / 8K\"\", \"\"Refresh rate in Hz\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"screen diagonal in inches marked with '.\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the screen diagonal, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name without the screen diagonal, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Networking equipment",
|
||||||
|
"aliases": [
|
||||||
|
"Mrežna oprema"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Other accessories",
|
||||||
|
"aliases": [
|
||||||
|
"Ostali dodatki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case (omit when no suitable term exists)\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"colour as a single word (when several colours are possible)\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour}</p>\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Other appliances",
|
||||||
|
"aliases": [
|
||||||
|
"Ostali aparati"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Other gaming accessories",
|
||||||
|
"aliases": [
|
||||||
|
"Ostali gaming dodatki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ovens",
|
||||||
|
"aliases": [
|
||||||
|
"Pečice"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Paddleboards",
|
||||||
|
"aliases": [
|
||||||
|
"Supi"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Personal care",
|
||||||
|
"aliases": [
|
||||||
|
"Osebna nega"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pools",
|
||||||
|
"aliases": [
|
||||||
|
"Bazeni"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"dimensions [cm]\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the dimensions, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the dimensions}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Power banks",
|
||||||
|
"aliases": [
|
||||||
|
"Prenosne baterije"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"charging power\"\", \"\"model\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Power tools",
|
||||||
|
"aliases": [
|
||||||
|
"Orodje za mojstre"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pressure washers",
|
||||||
|
"aliases": [
|
||||||
|
"Visokotlačni čistilci"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Printers",
|
||||||
|
"aliases": [
|
||||||
|
"Tiskalniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Projectors",
|
||||||
|
"aliases": [
|
||||||
|
"Projektorji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Refrigerators",
|
||||||
|
"aliases": [
|
||||||
|
"Hladilniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Refurbished laptops",
|
||||||
|
"aliases": [
|
||||||
|
"Obnovljeni prenosniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"processor\"\", \"\"graphics card\"\", \"\"memory\"\", \"\"dimensions\"\", \"\"refresh rate (when it is 144Hz)\"\", \"\"SD / HD / FHD / UHD / 4K / 8K\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the product type in sentence case and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the product type in lowercase}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Refurbished phones",
|
||||||
|
"aliases": [
|
||||||
|
"Obnovljeni telefoni"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"memory size\"\", colour as a single word\"\", \"\"obnovljen\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the colour and without the refurbished status, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour and without the refurbished status}</p>\n<H2>{Write the New product name without the colour and without the refurbished status, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour and without the refurbished status}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour and without the refurbished status}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Remote-controlled toys",
|
||||||
|
"aliases": [
|
||||||
|
"Igrače na daljinsko vodenje"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ride-on toys",
|
||||||
|
"aliases": [
|
||||||
|
"Otroška vozila"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Robot vacuums",
|
||||||
|
"aliases": [
|
||||||
|
"Robotski sesalniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Scooters and skateboards",
|
||||||
|
"aliases": [
|
||||||
|
"Skiroji in rolke"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"colour as a single word (when several colour variants exist)\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Screen protectors",
|
||||||
|
"aliases": [
|
||||||
|
"Zaščitna stekla"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"product brand\"\", \"\"brand and model of the device it is intended for\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Security cameras",
|
||||||
|
"aliases": [
|
||||||
|
"Nadzorne kamere"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sensors",
|
||||||
|
"aliases": [
|
||||||
|
"Senzorji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Shavers",
|
||||||
|
"aliases": [
|
||||||
|
"Brivniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Shredders",
|
||||||
|
"aliases": [
|
||||||
|
"Uničevalci dokumentov"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"PRODUCT DESCRIPTION\"};\r\nOld_product_name: {\"OLD PRODUCT NAME\"};\r\n\r\nUse the product template below.\r\n\r\nFollow this product template sentence by sentence and compose a new product description:\r\n\r\nProduct template:\r\n<name>{Write a new product name using this formula: \"\"Shredder\"\", \"brand with correct capitalisation\", \"product model in uppercase\". Do not use commas.}</name>\r\n\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n<H2>{Highlight one benefit and DO include the New product name}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and DO include the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\r\n<b>Technical specifications</b>\r\n<ul>\r\n<li>{Write 3-10 technical specifications as bullet points}</li>\r\n</ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Slides and swings",
|
||||||
|
"aliases": [
|
||||||
|
"Tobogani in gugalnice"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Smart helmets",
|
||||||
|
"aliases": [
|
||||||
|
"Pametne čelade"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"colour as a single word\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour}</p>\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Smart plugs",
|
||||||
|
"aliases": [
|
||||||
|
"Vtičnice"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"socket type (e.g. RJ45)\"\", \"\"product type in lowercase\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Smartwatches",
|
||||||
|
"aliases": [
|
||||||
|
"Pametne ure"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"colour as a single word\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour}</p>\n<H2>{Write the New product name without the colour, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the colour}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Solar panels",
|
||||||
|
"aliases": [
|
||||||
|
"Sončne celice"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Soundbars",
|
||||||
|
"aliases": [
|
||||||
|
"Soundbar zvočniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Steam cleaners",
|
||||||
|
"aliases": [
|
||||||
|
"Parni čistilci"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Strollers",
|
||||||
|
"aliases": [
|
||||||
|
"Vozički"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tabletop grills and toasters",
|
||||||
|
"aliases": [
|
||||||
|
"Namizni žari in opekači"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tablets",
|
||||||
|
"aliases": [
|
||||||
|
"Tablični računalniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"SD / HD / FHD / UHD / 4K / 8K\"\", \"\"memory size\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the product type in sentence case and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the product type in lowercase}</p>\n<H2>{Write a heading that highlights one benefit and DOES include the product type in lowercase.}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Televisions",
|
||||||
|
"aliases": [
|
||||||
|
"Televizorji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"SD / HD / FHD / UHD / 4K / 8K\"\", \"\"TV\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"screen diagonal in inches marked with '.\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the screen diagonal, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name without the screen diagonal, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Toys for infants",
|
||||||
|
"aliases": [
|
||||||
|
"Igrače za najmlajše"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Trampolines",
|
||||||
|
"aliases": [
|
||||||
|
"Trampolini"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas. Do not include the colour in the name.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tumble dryers",
|
||||||
|
"aliases": [
|
||||||
|
"Sušilni stroji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TV accessories",
|
||||||
|
"aliases": [
|
||||||
|
"Dodatki za televizorje"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TV mounts",
|
||||||
|
"aliases": [
|
||||||
|
"Nosilci za TV"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vacuum cleaners",
|
||||||
|
"aliases": [
|
||||||
|
"Sesalniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Wall chargers",
|
||||||
|
"aliases": [
|
||||||
|
"Hišni polnilniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Washer dryers",
|
||||||
|
"aliases": [
|
||||||
|
"Pralno - sušilni stroji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Washing machines",
|
||||||
|
"aliases": [
|
||||||
|
"Pralni stroji"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\", \"\"wash capacity [kg]\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name without the wash capacity, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the wash capacity}</p>\n<H2>{Write the New product name without the wash capacity, and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the wash capacity}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name without the wash capacity}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Watch accessories",
|
||||||
|
"aliases": [
|
||||||
|
"Dodatki za ure"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas. Do not include the colour in the name.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Water flossers",
|
||||||
|
"aliases": [
|
||||||
|
"Zobne prhe"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"brand with correct capitalisation\"\", \"\"product type in lowercase\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p>\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Water heaters",
|
||||||
|
"aliases": [
|
||||||
|
"Grelniki vode"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\r\n\r\nOld_product_description: {\"PRODUCT DESCRIPTION\"};\r\nOld_product_name: {\"OLD PRODUCT NAME\"};\r\n\r\nUse the product template below.\r\n\r\nProduct template:\r\n\r\n<name>{Write a new product name using this formula: \"brand with correct capitalisation\", \"water heater in lowercase\", \"product model in uppercase\", \"capacity in litres\". Do not use commas.}</name>\r\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\r\n\r\n<H2>{Write the New product name without the capacity, and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name without the capacity}</p>\r\n<H2>{Write the New product name without the capacity, and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\r\n<p>{Write a 100-word paragraph that DOES include the New product name without the capacity}</p>\r\n<H2>{Write the New product name without the capacity, and highlight one benefit}</H2>\r\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\r\n\r\n<b>Technical specifications</b>\r\n<ul>\r\n<li>{Write 3-10 technical specifications as bullet points}</li>\r\n</ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Window and floor cleaners",
|
||||||
|
"aliases": [
|
||||||
|
"Čistilci oken in tal"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Winter sports",
|
||||||
|
"aliases": [
|
||||||
|
"Zimski športi"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Wireless chargers",
|
||||||
|
"aliases": [
|
||||||
|
"Brezžični polnilniki"
|
||||||
|
],
|
||||||
|
"prompt": "Create a new product description in {{language}} using the following variables:\n\nOld_product_description: {\"\"PRODUCT DESCRIPTION\"\"};\nOld_product_name: {\"\"OLD PRODUCT NAME\"\"};\n\nUse the product template below. \n\nFollow this product template sentence by sentence and compose a new product description:\n\nProduct template:\n\n\n<name>{Write a new product name using this formula: \"\"product type in sentence case\"\", \"\"brand with correct capitalisation\"\", \"\"\"full product model, with the word and the ID in uppercase when available\"\"\". Do not use commas.}</name>\n<metaDescription>{Start with the New product name and then continue until you have written 140 characters including spaces}</metaDescription>\n\n<H2>{Write the New product name and highlight one benefit}</H2>\n<p>{Write a 100-word paragraph that does NOT contain the New product name.}</p>\n<H2>{Highlight one benefit and do NOT write the New product name}</H2>\n<p>{Write a 100-word paragraph that DOES include the New product name.}</p><b>{Technical specifications}</b><ul><li>{Write 3-10 technical specifications as bullet points}</li></ul>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -49,6 +49,11 @@ func DescriptionTemplateNeedsRepair(template any) bool {
|
|||||||
// name-first Slovenian instruction style with the SEO title length bound.
|
// name-first Slovenian instruction style with the SEO title length bound.
|
||||||
const DefaultLegacyMetaTitleRule = "Najprej napiši Novo ime izdelka in dodaj glavno prednost. Dolžina 50-60 znakov vključno s presledki."
|
const DefaultLegacyMetaTitleRule = "Najprej napiši Novo ime izdelka in dodaj glavno prednost. Dolžina 50-60 znakov vključno s presledki."
|
||||||
|
|
||||||
|
// DefaultMetaTitleRuleEN is the same instruction for the English platform-default
|
||||||
|
// formulas. Legacy dumps carry only a <metaDescription> rule, so the meta title
|
||||||
|
// instruction is supplied here in the language of the formula set.
|
||||||
|
const DefaultMetaTitleRuleEN = "Start with the New product name and add the main benefit. Length 50-60 characters including spaces."
|
||||||
|
|
||||||
// DeriveDescriptionFormulaFromLegacyParts builds description_template from split
|
// DeriveDescriptionFormulaFromLegacyParts builds description_template from split
|
||||||
// legacy DescriptionRules HTML + MetaRules. Legacy dumps never carried a meta
|
// legacy DescriptionRules HTML + MetaRules. Legacy dumps never carried a meta
|
||||||
// title rule, so meta intent (MetaRules present) also seeds a default metaTitle
|
// title rule, so meta intent (MetaRules present) also seeds a default metaTitle
|
||||||
@@ -198,7 +203,14 @@ func EffectiveDescriptionFormula(stored any, categoryPrompt string) DescriptionF
|
|||||||
WasLegacy: true,
|
WasLegacy: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(derived.Sections) > len(out.Sections) {
|
// A legacy HTML formula in the category prompt is the tenant's own authored
|
||||||
|
// structure, so it wins outright — it is also what buildA1StyleEnhanceUserTemplate
|
||||||
|
// puts in the GPT predloga. Preferring it only when it has MORE sections let a
|
||||||
|
// stale stored template (e.g. seven English boilerplate sections from an old
|
||||||
|
// migration) validate against a five-section prompt: the model obeyed the prompt,
|
||||||
|
// the gate rejected the reply as formula-mismatch, and enhance fell back to synth
|
||||||
|
// or supplier copy. Instruction and gate must read the same formula.
|
||||||
|
if len(derived.Sections) > 0 {
|
||||||
out.Sections = derived.Sections
|
out.Sections = derived.Sections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package aiprompts
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Regression: A1 categories carry both a legacy HTML formula in categories.prompt
|
||||||
|
// and a stale description_template written by an old migration (seven English
|
||||||
|
// boilerplate sections). Preferring whichever side had MORE sections handed the
|
||||||
|
// stale template to the validation gate while the model was instructed from the
|
||||||
|
// prompt — every reply then failed as "formula mismatch" and enhance fell back to
|
||||||
|
// synth or supplier copy. The tenant's own prompt formula must win.
|
||||||
|
func TestEffectiveDescriptionFormula_promptFormulaBeatsStaleStoredSections(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
prompt := SectionDescriptionStart + "\n" +
|
||||||
|
`<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>` +
|
||||||
|
`<p>{Napiši odstavek, ki je dolg 100 besed.}</p>` +
|
||||||
|
`<H2>{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" +
|
||||||
|
SectionDescriptionEnd
|
||||||
|
|
||||||
|
stale := map[string]any{"sections": []any{
|
||||||
|
map[string]any{"type": "h1", "instructions": "Write a compelling main title."},
|
||||||
|
map[string]any{"type": "p", "instructions": "Write a detailed paragraph about specific features."},
|
||||||
|
map[string]any{"type": "h2", "instructions": "Create a section heading."},
|
||||||
|
map[string]any{"type": "ul", "instructions": "List key features or specifications."},
|
||||||
|
map[string]any{"type": "p", "instructions": "Izpostavi eno prednost izdelka."},
|
||||||
|
map[string]any{"type": "p", "instructions": "Izpostavi eno prednost izdelka."},
|
||||||
|
map[string]any{"type": "p", "instructions": "Izpostavi eno prednost izdelka."},
|
||||||
|
}}
|
||||||
|
|
||||||
|
got := EffectiveDescriptionFormula(stale, prompt)
|
||||||
|
want := []string{"h2", "p", "h2", "p", "ul"}
|
||||||
|
if len(got.Sections) != len(want) {
|
||||||
|
t.Fatalf("got %d sections, want the %d from the prompt formula: %+v",
|
||||||
|
len(got.Sections), len(want), got.Sections)
|
||||||
|
}
|
||||||
|
for i, typ := range want {
|
||||||
|
if got.Sections[i].Type != typ {
|
||||||
|
t.Fatalf("section %d type=%q want %q (%+v)", i, got.Sections[i].Type, typ, got.Sections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without a legacy formula in the prompt, the stored template still applies.
|
||||||
|
func TestEffectiveDescriptionFormula_keepsStoredWhenPromptHasNoFormula(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
stored := map[string]any{"sections": []any{
|
||||||
|
map[string]any{"type": "h2", "instructions": "Heading"},
|
||||||
|
map[string]any{"type": "p", "instructions": "Body"},
|
||||||
|
}}
|
||||||
|
got := EffectiveDescriptionFormula(stored, "")
|
||||||
|
if len(got.Sections) != 2 || got.Sections[0].Type != "h2" {
|
||||||
|
t.Fatalf("stored template must survive an empty prompt: %+v", got.Sections)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,33 @@ import (
|
|||||||
var (
|
var (
|
||||||
reLegacyNameTag = regexp.MustCompile(`(?is)<\s*name\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*name\s*>`)
|
reLegacyNameTag = regexp.MustCompile(`(?is)<\s*name\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*name\s*>`)
|
||||||
reLegacyMetaTag = regexp.MustCompile(`(?is)<\s*metaDescription\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*metaDescription\s*>`)
|
reLegacyMetaTag = regexp.MustCompile(`(?is)<\s*metaDescription\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*metaDescription\s*>`)
|
||||||
reLegacyDescPH = regexp.MustCompile(`(?i)\{\s*""?\s*OPIS\s+IZDELKA\s*""?\s*\}`)
|
reLegacyDescPH = regexp.MustCompile(`(?i)\{\s*""?\s*(?:OPIS\s+IZDELKA|PRODUCT\s+DESCRIPTION)\s*""?\s*\}`)
|
||||||
reLegacyNamePH = regexp.MustCompile(`(?i)\{\s*""?\s*STARO\s+IME\s+IZDELKA\s*""?\s*\}`)
|
reLegacyNamePH = regexp.MustCompile(`(?i)\{\s*""?\s*(?:STARO\s+IME\s+IZDELKA|OLD\s+PRODUCT\s+NAME)\s*""?\s*\}`)
|
||||||
reDoubledQuotes = regexp.MustCompile(`"{2,}`)
|
reDoubledQuotes = regexp.MustCompile(`"{2,}`)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// legacyTemplateMarkers introduce the formula body in a combined prompt. The
|
||||||
|
// Slovenian marker is the legacy A1 wording; the English one is used by the
|
||||||
|
// platform-default seed (scripts/seed/default-category-prompts.json), which is the
|
||||||
|
// same template style translated.
|
||||||
|
var legacyTemplateMarkers = []string{"gpt predloga:", "product template:"}
|
||||||
|
|
||||||
|
// legacyIntroLinePrefixes are scaffolding lines above the template body ("here are
|
||||||
|
// the variables", "follow the template step by step") in either language. They are
|
||||||
|
// not body structure and must not become description sections.
|
||||||
|
var legacyIntroLinePrefixes = []string{
|
||||||
|
"ustvari nov opis",
|
||||||
|
"star_opis_izdelka",
|
||||||
|
"staro_ime_izdelka",
|
||||||
|
"uporabi spodnjo gpt",
|
||||||
|
"sledi tej gpt",
|
||||||
|
"create a new product description",
|
||||||
|
"old_product_description",
|
||||||
|
"old_product_name",
|
||||||
|
"use the product template",
|
||||||
|
"follow this product template",
|
||||||
|
}
|
||||||
|
|
||||||
// LegacyEnhanceParts holds extracted role content from a combined PHP/A1 category Prompt.
|
// LegacyEnhanceParts holds extracted role content from a combined PHP/A1 category Prompt.
|
||||||
type LegacyEnhanceParts struct {
|
type LegacyEnhanceParts struct {
|
||||||
TitleRules string
|
TitleRules string
|
||||||
@@ -22,7 +44,8 @@ type LegacyEnhanceParts struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsLegacyCombinedEnhancePrompt reports whether prompt looks like the old A1/PHP
|
// IsLegacyCombinedEnhancePrompt reports whether prompt looks like the old A1/PHP
|
||||||
// combined name+description(+meta) marketing blob (<name>/<metaDescription>/GPT predloga).
|
// combined name+description(+meta) marketing blob (<name>/<metaDescription>/GPT
|
||||||
|
// predloga), or its English platform-default translation (Product template:).
|
||||||
func IsLegacyCombinedEnhancePrompt(prompt string) bool {
|
func IsLegacyCombinedEnhancePrompt(prompt string) bool {
|
||||||
p := strings.TrimSpace(prompt)
|
p := strings.TrimSpace(prompt)
|
||||||
if p == "" {
|
if p == "" {
|
||||||
@@ -32,14 +55,20 @@ func IsLegacyCombinedEnhancePrompt(prompt string) bool {
|
|||||||
// `do NOT … <name>/<metaDescription> tags` in CategoryEnhanceUserTemplate.
|
// `do NOT … <name>/<metaDescription> tags` in CategoryEnhanceUserTemplate.
|
||||||
hasNameTag := reLegacyNameTag.MatchString(p)
|
hasNameTag := reLegacyNameTag.MatchString(p)
|
||||||
hasMetaTag := reLegacyMetaTag.MatchString(p)
|
hasMetaTag := reLegacyMetaTag.MatchString(p)
|
||||||
if hasNameTag && (hasMetaTag || strings.Contains(strings.ToLower(p), "gpt predloga")) {
|
hasTemplateMarker := hasAnyLegacyTemplateMarker(p)
|
||||||
|
hasPlaceholders := reLegacyNamePH.MatchString(p) || reLegacyDescPH.MatchString(p)
|
||||||
|
if hasNameTag && (hasMetaTag || hasTemplateMarker || hasPlaceholders) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if hasNameTag && (strings.Contains(p, "STARO IME IZDELKA") || strings.Contains(p, "OPIS IZDELKA")) {
|
return hasPlaceholders && hasTemplateMarker
|
||||||
return true
|
}
|
||||||
}
|
|
||||||
if strings.Contains(p, "STARO IME IZDELKA") && strings.Contains(p, "OPIS IZDELKA") && strings.Contains(strings.ToLower(p), "gpt predloga") {
|
func hasAnyLegacyTemplateMarker(prompt string) bool {
|
||||||
return true
|
lower := strings.ToLower(prompt)
|
||||||
|
for _, marker := range legacyTemplateMarkers {
|
||||||
|
if strings.Contains(lower, marker) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -64,8 +93,11 @@ func ParseLegacyCombinedEnhancePrompt(prompt string) LegacyEnhanceParts {
|
|||||||
|
|
||||||
rest := reLegacyNameTag.ReplaceAllString(raw, "")
|
rest := reLegacyNameTag.ReplaceAllString(raw, "")
|
||||||
rest = reLegacyMetaTag.ReplaceAllString(rest, "")
|
rest = reLegacyMetaTag.ReplaceAllString(rest, "")
|
||||||
if idx := strings.Index(strings.ToLower(rest), "gpt predloga:"); idx >= 0 {
|
for _, marker := range legacyTemplateMarkers {
|
||||||
rest = rest[idx+len("gpt predloga:"):]
|
if idx := strings.Index(strings.ToLower(rest), marker); idx >= 0 {
|
||||||
|
rest = rest[idx+len(marker):]
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Drop Slovenian boilerplate intro lines that are not body structure.
|
// Drop Slovenian boilerplate intro lines that are not body structure.
|
||||||
rest = stripLegacyIntroNoise(rest)
|
rest = stripLegacyIntroNoise(rest)
|
||||||
@@ -135,6 +167,15 @@ func ExtractEnhanceSectionBody(prompt, start, end string) string {
|
|||||||
return strings.TrimSpace(strings.TrimPrefix(strings.TrimSuffix(rest, "\n"), "\n"))
|
return strings.TrimSpace(strings.TrimPrefix(strings.TrimSuffix(rest, "\n"), "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasAnyPrefix(s string, prefixes []string) bool {
|
||||||
|
for _, p := range prefixes {
|
||||||
|
if strings.HasPrefix(s, p) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func cleanLegacyInstruction(s string) string {
|
func cleanLegacyInstruction(s string) string {
|
||||||
s = modernizeLegacyPlaceholders(s)
|
s = modernizeLegacyPlaceholders(s)
|
||||||
s = reDoubledQuotes.ReplaceAllString(s, `"`)
|
s = reDoubledQuotes.ReplaceAllString(s, `"`)
|
||||||
@@ -164,11 +205,7 @@ func stripLegacyIntroNoise(s string) string {
|
|||||||
if len(out) > 0 {
|
if len(out) > 0 {
|
||||||
out = append(out, "")
|
out = append(out, "")
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(lower, "ustvari nov opis"),
|
case hasAnyPrefix(lower, legacyIntroLinePrefixes):
|
||||||
strings.HasPrefix(lower, "star_opis_izdelka"),
|
|
||||||
strings.HasPrefix(lower, "staro_ime_izdelka"),
|
|
||||||
strings.HasPrefix(lower, "uporabi spodnjo gpt"),
|
|
||||||
strings.HasPrefix(lower, "sledi tej gpt"):
|
|
||||||
continue
|
continue
|
||||||
default:
|
default:
|
||||||
out = append(out, trim)
|
out = append(out, trim)
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
reQuotedNameSlot = regexp.MustCompile(`"([^"]+)"`)
|
reQuotedNameSlot = regexp.MustCompile(`"([^"]+)"`)
|
||||||
reNameFormulaTail = regexp.MustCompile(`(?is)formuli\s*:\s*(.*?)(?:\.?\s*Ne uporabljaj|\.?$)`)
|
// Slovenian legacy ("… po formuli: … Ne uporabljaj vejic.") and the English
|
||||||
|
// platform-default translation ("… using this formula: … Do not use commas.").
|
||||||
|
reNameFormulaTail = regexp.MustCompile(`(?is)formuli\s*:\s*(.*?)(?:\.?\s*Ne uporabljaj|\.?$)`)
|
||||||
|
reNameFormulaTailEN = regexp.MustCompile(`(?is)formula\s*:\s*(.*?)(?:\.?\s*Do not use|\.?$)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// TitleFormulaElement is one ordered slot in categories.title_template.
|
// TitleFormulaElement is one ordered slot in categories.title_template.
|
||||||
@@ -183,6 +186,8 @@ func extractLegacyNameSlots(rules string) []string {
|
|||||||
body := rules
|
body := rules
|
||||||
if m := reNameFormulaTail.FindStringSubmatch(rules); len(m) == 2 {
|
if m := reNameFormulaTail.FindStringSubmatch(rules); len(m) == 2 {
|
||||||
body = m[1]
|
body = m[1]
|
||||||
|
} else if m := reNameFormulaTailEN.FindStringSubmatch(rules); len(m) == 2 {
|
||||||
|
body = m[1]
|
||||||
}
|
}
|
||||||
raw := reQuotedNameSlot.FindAllStringSubmatch(body, -1)
|
raw := reQuotedNameSlot.FindAllStringSubmatch(body, -1)
|
||||||
out := make([]string, 0, len(raw))
|
out := make([]string, 0, len(raw))
|
||||||
@@ -203,6 +208,40 @@ func mapLegacyNameSlot(slot string) (TitleFormulaElement, bool) {
|
|||||||
return TitleFormulaElement{}, false
|
return TitleFormulaElement{}, false
|
||||||
}
|
}
|
||||||
low := strings.ToLower(s)
|
low := strings.ToLower(s)
|
||||||
|
// English platform-default slots (defaults/default-category-prompts.json) are
|
||||||
|
// matched before the Slovenian legacy vocabulary. Slovenian rules below are
|
||||||
|
// deliberately unchanged: A1's derived templates are already stored, and adding
|
||||||
|
// keywords there would churn them on every repair pass.
|
||||||
|
switch {
|
||||||
|
case strings.Contains(low, "product model"), strings.Contains(low, "device model"),
|
||||||
|
strings.Contains(low, "model of the device"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "Model", Value: "product_model", Description: s, Example: "EHT6020"}, true
|
||||||
|
case strings.Contains(low, "brand"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "Brand", Value: "brand", Description: s, Example: "Samsung"}, true
|
||||||
|
case strings.Contains(low, "product type"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "Product type", Value: "product_type", Description: s, Example: "Bluetooth speaker"}, true
|
||||||
|
case strings.Contains(low, "graphics card"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "GPU", Value: "graficna_kartica", Description: s}, true
|
||||||
|
case strings.Contains(low, "processor"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "CPU", Value: "procesor", Description: s}, true
|
||||||
|
case strings.Contains(low, "memory"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "Memory", Value: "kapaciteta_ram_pomnilnika", Description: s}, true
|
||||||
|
case strings.Contains(low, "screen diagonal"), strings.Contains(low, "screen size"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "Diagonal", Value: "diagonala_zaslona", Description: s}, true
|
||||||
|
case strings.Contains(low, "refresh rate"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "Refresh rate", Value: "refresh_rate", Description: s}, true
|
||||||
|
case strings.Contains(low, "weight group"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "Weight group", Value: "weight_group", Description: s}, true
|
||||||
|
case strings.Contains(low, "wash capacity"), strings.Contains(low, "capacity"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "Capacity", Value: "kapaciteta", Description: s}, true
|
||||||
|
case strings.Contains(low, "connector type"), strings.Contains(low, "socket type"),
|
||||||
|
strings.Contains(low, "cable type"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "Connector", Value: "connector", Description: s}, true
|
||||||
|
case strings.Contains(low, "colour"), strings.Contains(low, "color"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "Color", Value: "barva", Description: s}, true
|
||||||
|
case strings.Contains(low, "dimensions"):
|
||||||
|
return TitleFormulaElement{Type: "variable", Label: "Dimensions", Value: "dimensions", Description: s}, true
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(low, "znamka"):
|
case strings.Contains(low, "znamka"):
|
||||||
return TitleFormulaElement{Type: "variable", Label: "Brand", Value: "brand", Description: s, Example: "Samsung"}, true
|
return TitleFormulaElement{Type: "variable", Label: "Brand", Value: "brand", Description: s, Example: "Samsung"}, true
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package catalog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultFormulaTarget is one freshly created category to seed formulas onto.
|
||||||
|
type DefaultFormulaTarget struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
UniqueID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// pgxExecer is the subset of pgxpool.Pool / pgx.Tx used here so the helper works
|
||||||
|
// inside the CSV import transaction as well as on the plain pool.
|
||||||
|
type pgxExecer interface {
|
||||||
|
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDefaultCategoryFormulasSQL fills prompt / title_template / description_template
|
||||||
|
// on newly created categories. Only empty columns are written, so a tenant that
|
||||||
|
// already authored a formula (or an A1 category carrying its Slovenian legacy
|
||||||
|
// prompt) is never overwritten.
|
||||||
|
const applyDefaultCategoryFormulasSQL = `
|
||||||
|
UPDATE categories AS c SET
|
||||||
|
prompt = CASE
|
||||||
|
WHEN c.prompt IS NULL OR c.prompt = '{}'::jsonb
|
||||||
|
THEN v.prompt::jsonb ELSE c.prompt END,
|
||||||
|
title_template = CASE
|
||||||
|
WHEN c.title_template IS NULL OR c.title_template::text IN ('null', '{}')
|
||||||
|
THEN NULLIF(v.title_template, '')::jsonb ELSE c.title_template END,
|
||||||
|
description_template = CASE
|
||||||
|
WHEN c.description_template IS NULL OR c.description_template::text IN ('null', '{}')
|
||||||
|
THEN NULLIF(v.description_template, '')::jsonb ELSE c.description_template END,
|
||||||
|
updated_at = now()
|
||||||
|
FROM (
|
||||||
|
SELECT * FROM unnest($2::uuid[], $3::text[], $4::text[], $5::text[])
|
||||||
|
AS t(id, prompt, title_template, description_template)
|
||||||
|
) AS v
|
||||||
|
WHERE c.id = v.id AND c.company_id = $1`
|
||||||
|
|
||||||
|
// ApplyDefaultCategoryFormulas gives newly created categories the platform-default
|
||||||
|
// formulas (English, same style as the A1 legacy templates) when they have none.
|
||||||
|
//
|
||||||
|
// New categories get a working title/description/meta formula out of the box
|
||||||
|
// instead of falling through to generic "1-3 paragraphs" copy. Existing rows are
|
||||||
|
// untouched — this only runs at create time. A1 categories keep their Slovenian
|
||||||
|
// prompts because seed-a1 writes them and the SQL never overwrites a set column.
|
||||||
|
func ApplyDefaultCategoryFormulas(ctx context.Context, db pgxExecer, companyID uuid.UUID, targets ...DefaultFormulaTarget) error {
|
||||||
|
if db == nil || len(targets) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ids := make([]uuid.UUID, 0, len(targets))
|
||||||
|
prompts := make([]string, 0, len(targets))
|
||||||
|
titles := make([]string, 0, len(targets))
|
||||||
|
descs := make([]string, 0, len(targets))
|
||||||
|
for _, t := range targets {
|
||||||
|
if t.ID == uuid.Nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
def := aiprompts.DefaultCategoryFormula(t.UniqueID, t.Name)
|
||||||
|
if def.Overlay == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// "*" applies to every content language: the formula text is English while
|
||||||
|
// {{language}} still selects the output language at render time.
|
||||||
|
promptJSON, err := company.EncodeLangPromptMap(company.LangPromptMap{"*": def.Overlay})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ids = append(ids, t.ID)
|
||||||
|
prompts = append(prompts, string(promptJSON))
|
||||||
|
titles = append(titles, def.TitleTemplateJSON)
|
||||||
|
descs = append(descs, def.DescriptionTemplateJSON)
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := db.Exec(ctx, applyDefaultCategoryFormulasSQL, companyID, ids, prompts, titles, descs)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -237,6 +237,13 @@ func (s *Service) flushCategoryBatch(ctx context.Context, companyID uuid.UUID, b
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
nameByUID := make(map[string]string, len(insUIDs))
|
||||||
|
for i, uid := range insUIDs {
|
||||||
|
if i < len(insNames) {
|
||||||
|
nameByUID[uid] = insNames[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var seedFormulas []DefaultFormulaTarget
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var id uuid.UUID
|
var id uuid.UUID
|
||||||
var uid, path string
|
var uid, path string
|
||||||
@@ -246,6 +253,7 @@ func (s *Service) flushCategoryBatch(ctx context.Context, companyID uuid.UUID, b
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
known[uid] = categoryPathInfo{id: id, path: path, level: level}
|
known[uid] = categoryPathInfo{id: id, path: path, level: level}
|
||||||
|
seedFormulas = append(seedFormulas, DefaultFormulaTarget{ID: id, UniqueID: uid, Name: nameByUID[uid]})
|
||||||
res.Created++
|
res.Created++
|
||||||
}
|
}
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
@@ -253,6 +261,11 @@ func (s *Service) flushCategoryBatch(ctx context.Context, companyID uuid.UUID, b
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Imported categories get the platform-default formula too, so an imported
|
||||||
|
// taxonomy enhances into formula-shaped copy without further setup.
|
||||||
|
if err := ApplyDefaultCategoryFormulas(ctx, tx, companyID, seedFormulas...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
pendingInserts = next
|
pendingInserts = next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
||||||
@@ -167,6 +168,13 @@ func (s *Service) CreateCategory(ctx context.Context, companyID uuid.UUID, name,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// New categories start with the platform-default formula so enhance produces
|
||||||
|
// formula-shaped copy instead of generic paragraphs. Non-fatal: a category
|
||||||
|
// without a stored formula still falls back to the default at enhance time.
|
||||||
|
if err := ApplyDefaultCategoryFormulas(ctx, s.Pool, companyID,
|
||||||
|
DefaultFormulaTarget{ID: id, UniqueID: uniqueID, Name: name}); err != nil {
|
||||||
|
log.Printf("catalog: default formulas company=%s category=%s err=%v", companyID, id, err)
|
||||||
|
}
|
||||||
return s.GetCategory(ctx, companyID, id)
|
return s.GetCategory(ctx, companyID, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package company
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Regression: the generic invent shapes (" is a … product from …" and
|
||||||
|
// " je … znamk|kategor|produkt …") matched any length of copy. In Slovenian " je "
|
||||||
|
// appears in almost every sentence and A1 spec lists start with "Znamka: <brand>",
|
||||||
|
// so real formula output was classified as invent fallback, discarded, and replaced
|
||||||
|
// with the supplier feed text — Review then showed Original ≈ Enriched.
|
||||||
|
func TestLooksLikeHeuristicSynthesize_keepsRealFormulaCopy(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
slovenianFormulaCopy := "<h2>BRUNNER zložljiv stol ONE SHOT za udobje na poti</h2>" +
|
||||||
|
"<p>Zložljiv stol iz aluminija je zasnovan za dolge ure udobnega sedenja na terenu, na snemanju ali ob " +
|
||||||
|
"kampiranju. Okvir se razpre v enem gibu in ostane stabilen tudi na neravnih tleh, medtem ko tkanina " +
|
||||||
|
"sedišča prijetno diha in se hitro suši. Naslonjala za roke razbremenijo ramena, hrbtni del pa podpira " +
|
||||||
|
"naravno držo. Zaradi majhne teže ga brez napora prenesete od avtomobila do prizorišča.</p>" +
|
||||||
|
"<h2>Stabilnost in dolga življenjska doba</h2>" +
|
||||||
|
"<p>Konstrukcija združuje trpežne materiale in premišljene detajle. Aluminijasti profili so odporni proti " +
|
||||||
|
"koroziji, spoji pa so ojačani na mestih največjih obremenitev. Sivo črna kombinacija ostaja videti " +
|
||||||
|
"urejena tudi po sezoni uporabe na prostem, saj se madeži manj poznajo.</p>" +
|
||||||
|
"<ul><li>Znamka: BRUNNER</li><li>Model: ONE SHOT</li><li>Barva: sivo črna</li></ul>"
|
||||||
|
if LooksLikeHeuristicSynthesize(slovenianFormulaCopy) {
|
||||||
|
t.Fatal("real Slovenian formula copy must not read as invent fallback")
|
||||||
|
}
|
||||||
|
|
||||||
|
englishFormulaCopy := "<h2>Acme Monitor Pro</h2><p>This monitor is a strong choice for colour-critical work, " +
|
||||||
|
"with a factory-calibrated panel and a stand that adjusts through a wide range. Cable routing keeps the " +
|
||||||
|
"desk tidy, and the built-in hub means one cable to the laptop. Panel uniformity holds up across the " +
|
||||||
|
"whole surface, so gradients stay smooth from edge to edge in long editing sessions.</p>" +
|
||||||
|
"<ul><li>Brand: Acme</li><li>Key features: USB-C hub, height adjustment</li></ul>"
|
||||||
|
// "Key features" is ordinary section wording: suggestive enough to force a
|
||||||
|
// re-enhance, never enough to discard the copy.
|
||||||
|
if IsInventFallbackDescription(englishFormulaCopy) {
|
||||||
|
t.Fatal("real English formula copy must not read as invent fallback")
|
||||||
|
}
|
||||||
|
if IsInventFallbackDescription(slovenianFormulaCopy) {
|
||||||
|
t.Fatal("real Slovenian formula copy must not read as invent fallback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLooksLikeHeuristicSynthesize_stillCatchesInvent(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
cases := []string{
|
||||||
|
"Vogel's WALL 3245 is a TV Mounts product from Vogel's. Key specs: width 45 cm, max_load 40 kg.",
|
||||||
|
"Gorenje EHT6020 je izdelek znamke Gorenje.",
|
||||||
|
"Samsung QLED je izdelek v kategoriji Televizorji.",
|
||||||
|
"Acme Widget is a catalog product with the known attributes.",
|
||||||
|
"Acme Widget — catalog product.",
|
||||||
|
"BRUNNER ONE SHOT — Stolčki, znamka BRUNNER. Širina: 44 cm.",
|
||||||
|
"<h2>Gorenje EHT6020</h2><p>Gorenje EHT6020 — Štedilniki, znamka Gorenje.</p>" +
|
||||||
|
"<h3>Ključne lastnosti</h3><ul><li>Širina: 60 cm</li></ul>",
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if !LooksLikeHeuristicSynthesize(c) {
|
||||||
|
t.Fatalf("invent fallback must still be detected: %q", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,10 +31,12 @@ func IsWeakPriorEnhanceDescription(priorDesc string, titles ...string) bool {
|
|||||||
return reason == "weak" || reason == "title-echo"
|
return reason == "weak" || reason == "title-echo"
|
||||||
}
|
}
|
||||||
|
|
||||||
// heuristicSynthesizePhrases are distinctive invent / formula-skeleton snippets from
|
// heuristicSynthesizePhrases are distinctive invent snippets from
|
||||||
// synthesizeDescriptionFromTitle / synthesizeDescriptionFromFormula / factualDescriptionIntro.
|
// synthesizeDescriptionFromTitle / synthesizeDescriptionFromFormula / factualDescriptionIntro.
|
||||||
// These may look "strong" enough to pass IsWeakPriorEnhanceDescription but must never
|
// These may look "strong" enough to pass IsWeakPriorEnhanceDescription but must never
|
||||||
// hash-skip enhance (would leave fallback copy forever on reprocess).
|
// hash-skip enhance (would leave fallback copy forever on reprocess).
|
||||||
|
// Only whole sentences that no copywriter would produce belong here — anything a
|
||||||
|
// real category formula could legitimately emit goes in synthSkeletonPhrases.
|
||||||
var heuristicSynthesizePhrases = []string{
|
var heuristicSynthesizePhrases = []string{
|
||||||
"is a catalog product with the known attributes",
|
"is a catalog product with the known attributes",
|
||||||
"is listed in the ",
|
"is listed in the ",
|
||||||
@@ -43,19 +45,44 @@ var heuristicSynthesizePhrases = []string{
|
|||||||
"je izdelek v kategoriji",
|
"je izdelek v kategoriji",
|
||||||
"je izdelek znamke",
|
"je izdelek znamke",
|
||||||
". ključne specifikacije:",
|
". ključne specifikacije:",
|
||||||
|
" — katalogski izdelek",
|
||||||
|
" — catalog product",
|
||||||
|
}
|
||||||
|
|
||||||
|
// synthSkeletonPhrases also appear in synthesize output but are ordinary words a
|
||||||
|
// real description may use ("Ključne lastnosti" as a heading, "znamka X" in a spec
|
||||||
|
// line, "Key features" as a section title). They are suggestive, not proof, so only
|
||||||
|
// the hash-skip gate consults them — see IsInventFallbackDescription.
|
||||||
|
var synthSkeletonPhrases = []string{
|
||||||
// factualDescriptionIntro (post-phrase-avoidance invent)
|
// factualDescriptionIntro (post-phrase-avoidance invent)
|
||||||
" — znamka ",
|
" — znamka ",
|
||||||
", znamka ",
|
", znamka ",
|
||||||
" — katalogski izdelek",
|
|
||||||
" — from ",
|
" — from ",
|
||||||
" — catalog product",
|
|
||||||
// synthesizeDescriptionFromFormula heading fallbacks
|
// synthesizeDescriptionFromFormula heading fallbacks
|
||||||
"ključne lastnosti",
|
"ključne lastnosti",
|
||||||
"key features",
|
"key features",
|
||||||
}
|
}
|
||||||
|
|
||||||
// LooksLikeHeuristicSynthesize reports invent / formula-skeleton fallback copy.
|
// maxSynthSkeletonRunes bounds the suggestive signals above. Every synthesize helper
|
||||||
func LooksLikeHeuristicSynthesize(desc string) bool {
|
// emits at most a title heading, one intro sentence, and a short "Label: value"
|
||||||
|
// list, so it stays well under this; real category-formula prose (multiple 100-word
|
||||||
|
// paragraphs plus a spec list) runs several times longer.
|
||||||
|
const maxSynthSkeletonRunes = 600
|
||||||
|
|
||||||
|
// maxSynthInventRunes bounds the generic "<title> is a … product from …" /
|
||||||
|
// "<title> je … znamke …" shapes. Those describe a single invent sentence, so they
|
||||||
|
// may only match copy that is itself about one sentence long. Unbounded, " je "
|
||||||
|
// plus "znamk" matched almost any Slovenian description.
|
||||||
|
const maxSynthInventRunes = 2 * shortBoilerplateDescRunes
|
||||||
|
|
||||||
|
// IsInventFallbackDescription reports copy that is unmistakably machine invent —
|
||||||
|
// whole sentences emitted by the synthesize helpers, or the one-sentence
|
||||||
|
// "<title> is a <category> product from <brand>" shape.
|
||||||
|
//
|
||||||
|
// This is the strict test, safe for discarding a description outright. The looser
|
||||||
|
// LooksLikeHeuristicSynthesize adds ambiguous wording and is only used where a
|
||||||
|
// false positive costs one extra enhance call rather than the copy itself.
|
||||||
|
func IsInventFallbackDescription(desc string) bool {
|
||||||
plain := strings.ToLower(plainTextForWeakCheck(desc))
|
plain := strings.ToLower(plainTextForWeakCheck(desc))
|
||||||
if plain == "" {
|
if plain == "" {
|
||||||
return false
|
return false
|
||||||
@@ -65,19 +92,39 @@ func LooksLikeHeuristicSynthesize(desc string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len([]rune(plain)) > maxSynthInventRunes {
|
||||||
|
return false
|
||||||
|
}
|
||||||
// EN invent: "<title> is a <category> product from <brand>"
|
// EN invent: "<title> is a <category> product from <brand>"
|
||||||
if strings.Contains(plain, " is a ") && strings.Contains(plain, " product from ") {
|
if strings.Contains(plain, " is a ") && strings.Contains(plain, " product from ") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// SL/CS short invent: "<title> je … znamk|kategor|produkt …"
|
// SL/CS short invent: "<title> je … znamk|kategor|produkt …"
|
||||||
if strings.Contains(plain, " je ") &&
|
return strings.Contains(plain, " je ") &&
|
||||||
(strings.Contains(plain, "znamk") ||
|
(strings.Contains(plain, "znamk") ||
|
||||||
strings.Contains(plain, "kategor") ||
|
strings.Contains(plain, "kategor") ||
|
||||||
strings.Contains(plain, "produkt") ||
|
strings.Contains(plain, "produkt") ||
|
||||||
strings.Contains(plain, "televiz") ||
|
strings.Contains(plain, "televiz") ||
|
||||||
strings.Contains(plain, "monitor")) {
|
strings.Contains(plain, "monitor"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LooksLikeHeuristicSynthesize reports invent / formula-skeleton fallback copy for
|
||||||
|
// the enhance hash-skip gate: IsInventFallbackDescription plus wording that merely
|
||||||
|
// suggests a skeleton, inside a body short enough to be one. A false positive here
|
||||||
|
// forces one re-enhance, which is cheap; never use it to drop a description.
|
||||||
|
func LooksLikeHeuristicSynthesize(desc string) bool {
|
||||||
|
if IsInventFallbackDescription(desc) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
plain := strings.ToLower(plainTextForWeakCheck(desc))
|
||||||
|
if plain == "" || len([]rune(plain)) > maxSynthSkeletonRunes {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, p := range synthSkeletonPhrases {
|
||||||
|
if p != "" && strings.Contains(plain, p) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ import (
|
|||||||
// <metaDescription> from the HTML reply. v2 must recreate that GPT-predloga
|
// <metaDescription> from the HTML reply. v2 must recreate that GPT-predloga
|
||||||
// dominance while keeping JSON {"name","description","meta_*","attrs"}.
|
// dominance while keeping JSON {"name","description","meta_*","attrs"}.
|
||||||
|
|
||||||
const a1StyleEnhanceSystemTemplate = `Si tekstopisec, ki piše opise izdelkov v {{language}}.
|
// {{language}} renders an English label ("Slovenian"), so both templates name the
|
||||||
|
// language after a colon instead of inflecting it — legacy "v Slovenščini" read as
|
||||||
|
// "v Slovenian" and put a grammatical error in front of every A1 prompt.
|
||||||
|
const a1StyleEnhanceSystemTemplateSL = `Si tekstopisec, ki piše opise izdelkov v tem jeziku: {{language}}.
|
||||||
Rules:
|
Rules:
|
||||||
- Reply with ONLY JSON (no markdown)
|
- Reply with ONLY JSON (no markdown)
|
||||||
- Schema: {"name":"string","description":"string","meta_title":"string","meta_description":"string","attrs":{}}
|
- Schema: {"name":"string","description":"string","meta_title":"string","meta_description":"string","attrs":{}}
|
||||||
@@ -25,6 +28,20 @@ Rules:
|
|||||||
- attrs: only Allowed attribute keys when listed; fill from evidence only; omit unknowns
|
- attrs: only Allowed attribute keys when listed; fill from evidence only; omit unknowns
|
||||||
{{brand_voice}}`
|
{{brand_voice}}`
|
||||||
|
|
||||||
|
// a1StyleEnhanceSystemTemplateEN is the same contract for the English
|
||||||
|
// platform-default formulas, matching the English scaffolding in a1StyleLabelsEN.
|
||||||
|
const a1StyleEnhanceSystemTemplateEN = `You are a copywriter who writes product descriptions in this language: {{language}}.
|
||||||
|
Rules:
|
||||||
|
- Reply with ONLY JSON (no markdown)
|
||||||
|
- Schema: {"name":"string","description":"string","meta_title":"string","meta_description":"string","attrs":{}}
|
||||||
|
- Obey the product template in the user message step by step
|
||||||
|
- "name" follows the <name> formula — never copy Old_product_name unchanged when the formula asks for a new name
|
||||||
|
- "description" is ONE HTML string covering each product template body section in order (tags matching type: h1/h2/h3/h4, p, ul) — do NOT wrap the reply in <name> or <metaDescription> tags
|
||||||
|
- "meta_title" and "meta_description" are plain SEO text from the meta / <metaDescription> formula (never HTML body)
|
||||||
|
- Use only the supplied Old_product_name, Old_product_description, Category and Attrs as facts; never invent specifications
|
||||||
|
- attrs: only Allowed attribute keys when listed; fill from evidence only; omit unknowns
|
||||||
|
{{brand_voice}}`
|
||||||
|
|
||||||
var (
|
var (
|
||||||
reLegacyNameCapture = regexp.MustCompile(`(?is)<\s*name\s*>\s*(.*?)\s*<\s*/\s*name\s*>`)
|
reLegacyNameCapture = regexp.MustCompile(`(?is)<\s*name\s*>\s*(.*?)\s*<\s*/\s*name\s*>`)
|
||||||
reLegacyMetaCapture = regexp.MustCompile(`(?is)<\s*metaDescription\s*>\s*(.*?)\s*<\s*/\s*metaDescription\s*>`)
|
reLegacyMetaCapture = regexp.MustCompile(`(?is)<\s*metaDescription\s*>\s*(.*?)\s*<\s*/\s*metaDescription\s*>`)
|
||||||
@@ -60,7 +77,7 @@ func categoryUsesA1StyleFormula(catPrompt string, titleTemplate, descriptionTemp
|
|||||||
// buildA1StyleEnhanceUserTemplate rebuilds the legacy GPT-predloga user message
|
// buildA1StyleEnhanceUserTemplate rebuilds the legacy GPT-predloga user message
|
||||||
// (Title → <name>, Meta → <metaDescription>, Description HTML blocks) plus product
|
// (Title → <name>, Meta → <metaDescription>, Description HTML blocks) plus product
|
||||||
// evidence placeholders, asking for JSON field mapping.
|
// evidence placeholders, asking for JSON field mapping.
|
||||||
func buildA1StyleEnhanceUserTemplate(catPrompt string, titleTemplate, descriptionTemplate any, omitSEOMeta bool) string {
|
func buildA1StyleEnhanceUserTemplate(catPrompt string, titleTemplate, descriptionTemplate any, omitSEOMeta bool) (string, bool) {
|
||||||
titleInstr := strings.TrimSpace(aiprompts.TitleSectionInstructions(catPrompt))
|
titleInstr := strings.TrimSpace(aiprompts.TitleSectionInstructions(catPrompt))
|
||||||
if titleInstr == "" || isBuiltInTitleRoleBoilerplate(titleInstr) {
|
if titleInstr == "" || isBuiltInTitleRoleBoilerplate(titleInstr) {
|
||||||
titleInstr = ""
|
titleInstr = ""
|
||||||
@@ -105,37 +122,100 @@ func buildA1StyleEnhanceUserTemplate(catPrompt string, titleTemplate, descriptio
|
|||||||
predloga.WriteByte('\n')
|
predloga.WriteByte('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
var b strings.Builder
|
body := strings.TrimSpace(predloga.String())
|
||||||
b.WriteString("Ustvari nov opis izdelka v {{language}} z naslednjimi spremenljivkami:\n\n")
|
slovenian := reSlovenianFormulaCue.MatchString(body)
|
||||||
b.WriteString("Staro_ime_izdelka: {{name}}\n")
|
l := a1StyleLabelsEN
|
||||||
b.WriteString("Star_opis_izdelka: {{description}}\n")
|
if slovenian {
|
||||||
b.WriteString("Kategorija: {{category}}\n")
|
l = a1StyleLabelsSL
|
||||||
b.WriteString("Attrs: {{attrs}}\n\n")
|
|
||||||
b.WriteString("Uporabi spodnjo GPT predlogo.\n")
|
|
||||||
b.WriteString("Sledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka.\n\n")
|
|
||||||
b.WriteString("GPT predloga:\n\n")
|
|
||||||
b.WriteString(strings.TrimSpace(predloga.String()))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
b.WriteString("Odgovori SAMO z enim JSON objektom:\n")
|
|
||||||
b.WriteString(`- "name" = rezultat <name> formule (novo ime; ne kopiraj Staro_ime_izdelka, če formula zahteva novo ime)` + "\n")
|
|
||||||
b.WriteString(`- "description" = HTML telo po GPT predlogi (vsi razdelki po vrsti), BREZ <name>/<metaDescription> tagov` + "\n")
|
|
||||||
if !omitSEOMeta {
|
|
||||||
b.WriteString(`- "meta_title" / "meta_description" = plain SEO po meta / <metaDescription> formuli` + "\n")
|
|
||||||
}
|
}
|
||||||
b.WriteString(`- "attrs" = atributi (samo Allowed attribute keys, če so navedeni)`)
|
|
||||||
|
var b strings.Builder
|
||||||
|
fmt.Fprintf(&b, "%s\n\n", l.intro)
|
||||||
|
fmt.Fprintf(&b, "%s: {{name}}\n", l.oldName)
|
||||||
|
fmt.Fprintf(&b, "%s: {{description}}\n", l.oldDesc)
|
||||||
|
fmt.Fprintf(&b, "%s: {{category}}\n", l.category)
|
||||||
|
b.WriteString("Attrs: {{attrs}}\n\n")
|
||||||
|
fmt.Fprintf(&b, "%s\n%s\n\n", l.useTemplate, l.followTemplate)
|
||||||
|
fmt.Fprintf(&b, "%s\n\n", l.templateHeader)
|
||||||
|
b.WriteString(body)
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
fmt.Fprintf(&b, "%s\n", l.replyHeader)
|
||||||
|
fmt.Fprintf(&b, "%s\n", l.nameRule)
|
||||||
|
fmt.Fprintf(&b, "%s\n", l.descRule)
|
||||||
|
if !omitSEOMeta {
|
||||||
|
fmt.Fprintf(&b, "%s\n", l.metaRule)
|
||||||
|
}
|
||||||
|
b.WriteString(l.attrsRule)
|
||||||
|
|
||||||
// Free-form category overlay (no role markers) — keep as extra guidance like
|
// Free-form category overlay (no role markers) — keep as extra guidance like
|
||||||
// soft "Category guidance" did, without replacing the GPT predloga.
|
// soft "Category guidance" did, without replacing the template.
|
||||||
extra := strings.TrimSpace(catPrompt)
|
extra := strings.TrimSpace(catPrompt)
|
||||||
if extra != "" &&
|
if extra != "" &&
|
||||||
!aiprompts.CategoryEnhanceHasRoleSections(extra) &&
|
!aiprompts.CategoryEnhanceHasRoleSections(extra) &&
|
||||||
!aiprompts.IsLegacyCombinedEnhancePrompt(extra) {
|
!aiprompts.IsLegacyCombinedEnhancePrompt(extra) {
|
||||||
b.WriteString("\n\nDodatna kategorijska navodila:\n")
|
fmt.Fprintf(&b, "\n\n%s\n", l.extraHeader)
|
||||||
b.WriteString(extra)
|
b.WriteString(extra)
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(b.String())
|
return strings.TrimSpace(b.String()), slovenian
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a1StyleLabels are the scaffolding strings around a category formula: how the
|
||||||
|
// supplied variables are labelled and how the reply is framed.
|
||||||
|
type a1StyleLabels struct {
|
||||||
|
intro string
|
||||||
|
oldName string
|
||||||
|
oldDesc string
|
||||||
|
category string
|
||||||
|
useTemplate string
|
||||||
|
followTemplate string
|
||||||
|
templateHeader string
|
||||||
|
replyHeader string
|
||||||
|
nameRule string
|
||||||
|
descRule string
|
||||||
|
metaRule string
|
||||||
|
attrsRule string
|
||||||
|
extraHeader string
|
||||||
|
}
|
||||||
|
|
||||||
|
var a1StyleLabelsSL = a1StyleLabels{
|
||||||
|
intro: "Ustvari nov opis izdelka v tem jeziku: {{language}}, z naslednjimi spremenljivkami:",
|
||||||
|
oldName: "Staro_ime_izdelka",
|
||||||
|
oldDesc: "Star_opis_izdelka",
|
||||||
|
category: "Kategorija",
|
||||||
|
useTemplate: "Uporabi spodnjo GPT predlogo.",
|
||||||
|
followTemplate: "Sledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka.",
|
||||||
|
templateHeader: "GPT predloga:",
|
||||||
|
replyHeader: "Odgovori SAMO z enim JSON objektom:",
|
||||||
|
nameRule: `- "name" = rezultat <name> formule (novo ime; ne kopiraj Staro_ime_izdelka, če formula zahteva novo ime)`,
|
||||||
|
descRule: `- "description" = HTML telo po GPT predlogi (vsi razdelki po vrsti), BREZ <name>/<metaDescription> tagov`,
|
||||||
|
metaRule: `- "meta_title" / "meta_description" = plain SEO po meta / <metaDescription> formuli`,
|
||||||
|
attrsRule: `- "attrs" = atributi (samo Allowed attribute keys, če so navedeni)`,
|
||||||
|
extraHeader: "Dodatna kategorijska navodila:",
|
||||||
|
}
|
||||||
|
|
||||||
|
var a1StyleLabelsEN = a1StyleLabels{
|
||||||
|
intro: "Create a new product description in {{language}} using the following variables:",
|
||||||
|
oldName: "Old_product_name",
|
||||||
|
oldDesc: "Old_product_description",
|
||||||
|
category: "Category",
|
||||||
|
useTemplate: "Use the product template below.",
|
||||||
|
followTemplate: "Follow this product template sentence by sentence and compose a new product description.",
|
||||||
|
templateHeader: "Product template:",
|
||||||
|
replyHeader: "Reply with ONE JSON object only:",
|
||||||
|
nameRule: `- "name" = the result of the <name> formula (a new name; do not copy Old_product_name when the formula asks for a new one)`,
|
||||||
|
descRule: `- "description" = the HTML body per the product template (every section in order), WITHOUT <name>/<metaDescription> tags`,
|
||||||
|
metaRule: `- "meta_title" / "meta_description" = plain SEO text per the meta / <metaDescription> formula`,
|
||||||
|
attrsRule: `- "attrs" = attributes (only Allowed attribute keys when listed)`,
|
||||||
|
extraHeader: "Additional category instructions:",
|
||||||
|
}
|
||||||
|
|
||||||
|
// reSlovenianFormulaCue keeps the scaffolding in the same language the formula
|
||||||
|
// speaks. A1's legacy formulas refer to "Novo ime izdelka" and were written against
|
||||||
|
// the Slovenian variable labels in generator.php, so those keep the Slovenian
|
||||||
|
// frame; the English platform defaults get the English one. Output language is set
|
||||||
|
// by {{language}} in the system prompt either way.
|
||||||
|
var reSlovenianFormulaCue = regexp.MustCompile(`(?i)novo[ _]ime[ _]izdelka|napiši|napisi|izpostavi|izdelka|znamka`)
|
||||||
|
|
||||||
func collapseLegacyInstr(s string) string {
|
func collapseLegacyInstr(s string) string {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ func TestBuildA1StyleEnhanceUserTemplate_mirrorsLegacyGenerator(t *testing.T) {
|
|||||||
if !categoryUsesA1StyleFormula(prompt, nil, descTpl) {
|
if !categoryUsesA1StyleFormula(prompt, nil, descTpl) {
|
||||||
t.Fatal("expected A1-style formula detection")
|
t.Fatal("expected A1-style formula detection")
|
||||||
}
|
}
|
||||||
user := buildA1StyleEnhanceUserTemplate(prompt, nil, descTpl, false)
|
user, slovenian := buildA1StyleEnhanceUserTemplate(prompt, nil, descTpl, false)
|
||||||
|
if !slovenian {
|
||||||
|
t.Fatal("Slovenian A1 formula must keep the Slovenian scaffolding")
|
||||||
|
}
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
"GPT predloga:",
|
"GPT predloga:",
|
||||||
"Sledi tej GPT predlogi",
|
"Sledi tej GPT predlogi",
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ type ProductInput struct {
|
|||||||
// CategoryUniqueID is the taxonomy unique_id for overlay/formula keys when
|
// CategoryUniqueID is the taxonomy unique_id for overlay/formula keys when
|
||||||
// the enhance display category argument is a localized name.
|
// the enhance display category argument is a localized name.
|
||||||
CategoryUniqueID string
|
CategoryUniqueID string
|
||||||
|
// CategoryDisplayName is the resolved category label. Together with
|
||||||
|
// CategoryUniqueID it selects the platform-default formula for categories that
|
||||||
|
// carry no prompt/formula of their own (aiprompts.DefaultCategoryFormula).
|
||||||
|
CategoryDisplayName string
|
||||||
// OmitSEOMeta skips meta_title / meta_description enhance + free-template fill
|
// OmitSEOMeta skips meta_title / meta_description enhance + free-template fill
|
||||||
// (A1 cohort does not use SEO meta fields).
|
// (A1 cohort does not use SEO meta fields).
|
||||||
OmitSEOMeta bool
|
OmitSEOMeta bool
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A categorised product with no formula of its own enhances through the English
|
||||||
|
// platform default rather than the generic "1-3 paragraphs" rule.
|
||||||
|
func TestWithDefaultCategoryFormula_appliesToCategorisedProduct(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got := withDefaultCategoryFormula(ProductInput{
|
||||||
|
CategoryUniqueID: "high-chairs",
|
||||||
|
CategoryDisplayName: "High chairs",
|
||||||
|
})
|
||||||
|
if got.CategoryEnhancePrompt == "" {
|
||||||
|
t.Fatal("expected the platform default overlay")
|
||||||
|
}
|
||||||
|
if FormatTitleFormulaConstraint(got.TitleTemplate) == "" {
|
||||||
|
t.Fatalf("expected a default title formula, got %#v", got.TitleTemplate)
|
||||||
|
}
|
||||||
|
if FormatDescriptionFormulaConstraint(got.DescriptionTemplate) == "" {
|
||||||
|
t.Fatalf("expected a default description formula, got %#v", got.DescriptionTemplate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithDefaultCategoryFormula_doesNotOverride(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
cases := map[string]ProductInput{
|
||||||
|
"no category at all": {},
|
||||||
|
"category prompt already set": {
|
||||||
|
CategoryUniqueID: "high-chairs",
|
||||||
|
CategoryEnhancePrompt: "--- Title ---\nKeep the supplier title.\n--- End Title ---",
|
||||||
|
},
|
||||||
|
"tenant-authored enhance template": {
|
||||||
|
CategoryUniqueID: "high-chairs",
|
||||||
|
EnhanceUserTemplate: "Write a haiku about {{name}}",
|
||||||
|
},
|
||||||
|
"category has its own title formula": {
|
||||||
|
CategoryUniqueID: "high-chairs",
|
||||||
|
TitleTemplate: map[string]any{"separator": " ", "elements": []any{
|
||||||
|
map[string]any{"type": "variable", "value": "product_type"},
|
||||||
|
map[string]any{"type": "variable", "value": "brand"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, in := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := withDefaultCategoryFormula(in)
|
||||||
|
if got.CategoryEnhancePrompt != in.CategoryEnhancePrompt {
|
||||||
|
t.Fatalf("platform default overrode an explicit choice: %q", got.CategoryEnhancePrompt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A1's Slovenian formulas keep the Slovenian scaffolding and system prompt they had
|
||||||
|
// in generator.php; the English defaults get an English frame. Output language is
|
||||||
|
// set by {{language}} in both cases.
|
||||||
|
func TestResolveProductPromptTemplates_scaffoldingFollowsFormulaLanguage(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
slovenian := aiprompts.SplitLegacyCombinedEnhancePrompt(a1StolckiLegacyPrompt)
|
||||||
|
sysSL, userSL := resolveProductPromptTemplates(ProductInput{
|
||||||
|
CategoryUniqueID: "stolcki",
|
||||||
|
CategoryDisplayName: "Stolčki",
|
||||||
|
CategoryEnhancePrompt: slovenian,
|
||||||
|
Language: "sl",
|
||||||
|
})
|
||||||
|
if !strings.Contains(sysSL, "Si tekstopisec") {
|
||||||
|
t.Fatalf("Slovenian formula must keep the Slovenian system prompt:\n%s", sysSL)
|
||||||
|
}
|
||||||
|
for _, want := range []string{"GPT predloga:", "Staro_ime_izdelka: {{name}}", "<name>{"} {
|
||||||
|
if !strings.Contains(userSL, want) {
|
||||||
|
t.Fatalf("Slovenian scaffolding missing %q:\n%s", want, userSL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sysEN, userEN := resolveProductPromptTemplates(ProductInput{
|
||||||
|
CategoryUniqueID: "high-chairs",
|
||||||
|
CategoryDisplayName: "High chairs",
|
||||||
|
})
|
||||||
|
if !strings.Contains(sysEN, "You are a copywriter") {
|
||||||
|
t.Fatalf("English default must use the English system prompt:\n%s", sysEN)
|
||||||
|
}
|
||||||
|
for _, want := range []string{"Product template:", "Old_product_name: {{name}}", "<name>{"} {
|
||||||
|
if !strings.Contains(userEN, want) {
|
||||||
|
t.Fatalf("English scaffolding missing %q:\n%s", want, userEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(userEN, "GPT predloga") || strings.Contains(userEN, "Staro_ime_izdelka") {
|
||||||
|
t.Fatalf("English default leaked Slovenian scaffolding:\n%s", userEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End to end through RunSteps: a category with no stored formula still produces
|
||||||
|
// formula-shaped HTML and a rebuilt name, and the feed is only used as evidence.
|
||||||
|
func TestRunSteps_defaultFormulaDrivesGeneration(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
reply, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "BRUNNER folding chair ONE SHOT 0404164N.C20",
|
||||||
|
"description": "<h2>BRUNNER folding chair for life on the road</h2><p>" +
|
||||||
|
strings.Repeat("A light aluminium frame opens in one motion and stays steady on uneven ground. ", 4) +
|
||||||
|
"</p><h2>Built to last a season outdoors</h2><p>" +
|
||||||
|
strings.Repeat("Reinforced joints take the load where it matters and wipe clean in seconds. ", 4) +
|
||||||
|
"</p><b>Technical specifications</b><ul><li>Brand: BRUNNER</li><li>Material: aluminium</li></ul>",
|
||||||
|
"attrs": map[string]any{"brand": "BRUNNER", "product_model": "ONE SHOT 0404164N.C20"},
|
||||||
|
})
|
||||||
|
c := &formulaRegressionCompleter{reply: string(reply)}
|
||||||
|
feedName := "BRUNNER folding camping chair ONE SHOT grey black 0404164N.C20"
|
||||||
|
feedDesc := "An elegant and very light chair designed for art directors. Folding aluminium frame and armrests."
|
||||||
|
|
||||||
|
out, err := (&Engine{Completer: c}).RunSteps(context.Background(), "co", ProductInput{
|
||||||
|
GTIN: "8022068075495",
|
||||||
|
Name: feedName,
|
||||||
|
Description: feedDesc,
|
||||||
|
Mapped: map[string]any{
|
||||||
|
"name": feedName, "description": feedDesc,
|
||||||
|
"brand": "BRUNNER", "category": "high-chairs",
|
||||||
|
"specifications": map[string]any{"Material": "aluminium"},
|
||||||
|
},
|
||||||
|
Language: "en",
|
||||||
|
ContentLanguages: []string{"en"},
|
||||||
|
CategoryNamesByUID: map[string]string{"high-chairs": "High chairs"},
|
||||||
|
}, "full", []string{"high-chairs"}, StepPolicy{AllowAI: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c.calls != 1 {
|
||||||
|
t.Fatalf("a compliant reply must not need a retry, calls=%d", c.calls)
|
||||||
|
}
|
||||||
|
if !strings.Contains(c.user, "Product template:") || !strings.Contains(c.user, "<H2>{") {
|
||||||
|
t.Fatalf("default formula never reached the prompt:\n%s", c.user)
|
||||||
|
}
|
||||||
|
if out.ProcessedName == out.Name || out.ProcessedDescription == out.Description {
|
||||||
|
t.Fatal("enriched output must differ from the feed")
|
||||||
|
}
|
||||||
|
if !strings.Contains(out.ProcessedDescription, "<h2>") || !strings.Contains(out.ProcessedDescription, "<ul>") {
|
||||||
|
t.Fatalf("processed description is not formula HTML: %q", out.ProcessedDescription)
|
||||||
|
}
|
||||||
|
if out.Name != feedName || out.Description != feedDesc {
|
||||||
|
t.Fatalf("Original must stay the feed: name=%q desc=%q", out.Name, out.Description)
|
||||||
|
}
|
||||||
|
if h, _ := out.FieldSources[FieldEnhanceInputHash].(string); h == "" {
|
||||||
|
t.Fatal("quality enhance under the default formula must persist a hash")
|
||||||
|
}
|
||||||
|
_ = company.DefaultLanguage
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Regressions for "enhance just copies the feed" on A1-style category formulas.
|
||||||
|
// Fixture is the reported product (GTIN 8022068075495, category Stolčki) with the
|
||||||
|
// committed A1 seed prompt and the formula columns as the live A1 catalog stores
|
||||||
|
// them: a brand-only title_template stub and a generic English description_template.
|
||||||
|
|
||||||
|
type formulaRegressionCompleter struct {
|
||||||
|
system, user string
|
||||||
|
calls int
|
||||||
|
reply string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *formulaRegressionCompleter) Complete(_ context.Context, system, user string) (Completion, error) {
|
||||||
|
c.calls++
|
||||||
|
c.system, c.user = system, user
|
||||||
|
return Completion{Text: c.reply, TotalTokens: 10}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *formulaRegressionCompleter) Enabled() bool { return true }
|
||||||
|
|
||||||
|
// a1StolckiLegacyPrompt is the "Stolčki" entry of scripts/seed/a1-category-prompts.json.
|
||||||
|
const a1StolckiLegacyPrompt = "Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\n" +
|
||||||
|
"Star_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\n" +
|
||||||
|
"Uporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n" +
|
||||||
|
"<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", " +
|
||||||
|
"\"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n" +
|
||||||
|
"<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n" +
|
||||||
|
"<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n" +
|
||||||
|
"<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n" +
|
||||||
|
"<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n" +
|
||||||
|
"<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>" +
|
||||||
|
"<b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>"
|
||||||
|
|
||||||
|
const a1BrandOnlyTitleTemplate = `{"separator":" ","elements":[` +
|
||||||
|
`{"id":"0-variable-brand","type":"variable","label":"Znamka","value":"brand","example":"Samsung"}]}`
|
||||||
|
|
||||||
|
const a1GenericEnglishDescTemplate = `{"metaTitle":"Create a concise and compelling Meta Title.",` +
|
||||||
|
`"metaDescription":"Write a short Meta Description.",` +
|
||||||
|
`"sections":[{"id":"a7908e85","type":"p","instructions":"Write a detailed paragraph about specific features or benefits."}]}`
|
||||||
|
|
||||||
|
const a1FeedName = "BRUNNER zlažljiv stol za kampiranje ONE SHOT sivo črn 0404164N.C20"
|
||||||
|
const a1FeedDesc = "Eleganten in lahek stol za umetniške direktorje. Zložljiv aluminijast okvir, udobno sedišče in naslon za roke."
|
||||||
|
const a1FormulaName = "BRUNNER zložljiv stol za kampiranje ONE SHOT 0404164N.C20"
|
||||||
|
|
||||||
|
// a1FormulaReply obeys the Stolčki formula: h2 + p + h2 + p + spec list, in
|
||||||
|
// Slovenian. Its prose contains " je " and its spec list contains "Znamka:" —
|
||||||
|
// the combination that used to be misread as invent fallback and discarded.
|
||||||
|
const a1FormulaReply = `{"name":"` + a1FormulaName + `",
|
||||||
|
"description":"<h2>BRUNNER zložljiv stol ONE SHOT za udobje na poti</h2><p>Zložljiv stol iz aluminija je zasnovan za dolge ure udobnega sedenja na terenu, na snemanju ali ob kampiranju. Okvir se razpre v enem gibu in ostane stabilen tudi na neravnih tleh, medtem ko tkanina sedišča prijetno diha in se hitro suši. Naslonjala za roke razbremenijo ramena, hrbtni del pa podpira naravno držo. Zaradi majhne teže ga brez napora prenesete od avtomobila do prizorišča, zložen pa zavzame le malo prostora v prtljažniku.</p><h2>Stabilnost in dolga življenjska doba</h2><p>Konstrukcija združuje trpežne materiale in premišljene detajle. Aluminijasti profili so odporni proti koroziji, spoji pa so ojačani na mestih največjih obremenitev. Sivo črna kombinacija ostaja videti urejena tudi po sezoni uporabe na prostem, saj se madeži manj poznajo. Stol je enostavno očistiti z vlažno krpo, po uporabi pa ga preprosto zložite in shranite.</p><b>Tehnične specifikacije</b><ul><li>Znamka: BRUNNER</li><li>Model: ONE SHOT 0404164N.C20</li><li>Barva: sivo črna</li><li>Material okvirja: aluminij</li></ul>",
|
||||||
|
"meta_title":"BRUNNER zložljiv stol ONE SHOT",
|
||||||
|
"meta_description":"BRUNNER zložljiv stol za kampiranje ONE SHOT je lahek in stabilen zložljiv stol z udobnim sediščem za teren.",
|
||||||
|
"attrs":{"brand":"BRUNNER","product_model":"ONE SHOT 0404164N.C20"}}`
|
||||||
|
|
||||||
|
func jsonObject(t *testing.T, raw string) map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
var m map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func a1ProductInput(t *testing.T, catPrompt string, titleTpl, descTpl any) ProductInput {
|
||||||
|
t.Helper()
|
||||||
|
in := ProductInput{
|
||||||
|
GTIN: "8022068075495",
|
||||||
|
Name: a1FeedName,
|
||||||
|
Description: a1FeedDesc,
|
||||||
|
Mapped: map[string]any{
|
||||||
|
"name": a1FeedName,
|
||||||
|
"description": a1FeedDesc,
|
||||||
|
"brand": "BRUNNER",
|
||||||
|
"gtin": "8022068075495",
|
||||||
|
"category": "stolcki",
|
||||||
|
"specifications": map[string]any{"Barva": "sivo črna", "Material": "aluminij"},
|
||||||
|
},
|
||||||
|
Raw: map[string]any{},
|
||||||
|
Language: "sl",
|
||||||
|
ContentLanguages: []string{"sl"},
|
||||||
|
CategoryFormulasByKey: map[string]CategoryFormulas{
|
||||||
|
"stolcki": {TitleTemplate: titleTpl, DescriptionTemplate: descTpl},
|
||||||
|
},
|
||||||
|
CategoryNamesByUID: map[string]string{"stolcki": "Stolčki"},
|
||||||
|
OmitSEOMeta: true,
|
||||||
|
}
|
||||||
|
if catPrompt != "" {
|
||||||
|
in.CategoryPromptsByLang = map[string]company.LangPromptMap{
|
||||||
|
"stolcki": {"sl": catPrompt, "*": catPrompt},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formula-compliant Slovenian copy must reach processed_* on every processing type
|
||||||
|
// the dashboard starts. It used to be dropped by the invent heuristic, leaving the
|
||||||
|
// supplier feed text on both sides of Review ("Matched").
|
||||||
|
func TestRunSteps_A1FormulaCopyReachesProcessedFields(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
catPrompt := aiprompts.SplitLegacyCombinedEnhancePrompt(a1StolckiLegacyPrompt)
|
||||||
|
in := a1ProductInput(t, catPrompt,
|
||||||
|
jsonObject(t, a1BrandOnlyTitleTemplate), jsonObject(t, a1GenericEnglishDescTemplate))
|
||||||
|
|
||||||
|
// "title" / "description" / "enhance" are what products/+page.svelte sends.
|
||||||
|
for _, ptype := range []string{"full", "enhance", "title", "description"} {
|
||||||
|
t.Run(ptype, func(t *testing.T) {
|
||||||
|
c := &formulaRegressionCompleter{reply: a1FormulaReply}
|
||||||
|
out, err := (&Engine{Completer: c}).RunSteps(
|
||||||
|
context.Background(), "co", in, ptype, []string{"stolcki"}, StepPolicy{AllowAI: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c.calls != 1 {
|
||||||
|
t.Fatalf("compliant reply must not trigger a formula retry, calls=%d", c.calls)
|
||||||
|
}
|
||||||
|
if out.ProcessedName != a1FormulaName {
|
||||||
|
t.Fatalf("ProcessedName=%q want the formula name %q", out.ProcessedName, a1FormulaName)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out.ProcessedDescription, "<h2>") ||
|
||||||
|
!strings.Contains(out.ProcessedDescription, "<ul>") {
|
||||||
|
t.Fatalf("ProcessedDescription lost the formula HTML: %q", out.ProcessedDescription)
|
||||||
|
}
|
||||||
|
if out.ProcessedDescription == out.Description || out.ProcessedName == out.Name {
|
||||||
|
t.Fatal("Review would show Matched — enriched must differ from Original")
|
||||||
|
}
|
||||||
|
if out.Name != a1FeedName || out.Description != a1FeedDesc {
|
||||||
|
t.Fatalf("Original must stay supplier copy: name=%q desc=%q", out.Name, out.Description)
|
||||||
|
}
|
||||||
|
if h, _ := out.FieldSources[FieldEnhanceInputHash].(string); h == "" {
|
||||||
|
t.Fatal("quality enhance must persist enhance_input_hash so reprocess can skip")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The formula only keys when enhance is fed a category and attributes. Partial
|
||||||
|
// enhance types used to run normalize+ai_enhance alone, so the prompt carried an
|
||||||
|
// empty Attrs line and (for feeds without a mapped category) no category at all.
|
||||||
|
func TestRunSteps_partialEnhanceStillFeedsFormulaInputs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
catPrompt := aiprompts.SplitLegacyCombinedEnhancePrompt(a1StolckiLegacyPrompt)
|
||||||
|
in := a1ProductInput(t, catPrompt,
|
||||||
|
jsonObject(t, a1BrandOnlyTitleTemplate), jsonObject(t, a1GenericEnglishDescTemplate))
|
||||||
|
|
||||||
|
for _, ptype := range []string{"enhance", "title", "description"} {
|
||||||
|
t.Run(ptype, func(t *testing.T) {
|
||||||
|
c := &formulaRegressionCompleter{reply: a1FormulaReply}
|
||||||
|
if _, err := (&Engine{Completer: c}).RunSteps(
|
||||||
|
context.Background(), "co", in, ptype, []string{"stolcki"}, StepPolicy{AllowAI: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(c.user, "Kategorija: Stolčki") {
|
||||||
|
t.Fatalf("category missing from enhance prompt:\n%s", c.user)
|
||||||
|
}
|
||||||
|
if !strings.Contains(c.user, `"brand":"BRUNNER"`) {
|
||||||
|
t.Fatalf("attrs missing from enhance prompt:\n%s", c.user)
|
||||||
|
}
|
||||||
|
if !strings.Contains(c.user, "GPT predloga:") || !strings.Contains(c.user, "<name>{") {
|
||||||
|
t.Fatalf("category formula missing from enhance prompt:\n%s", c.user)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A model that echoes the supplier name/description must never be recorded as a
|
||||||
|
// successful enhance: no enhance_input_hash (so reprocess retries) and Original
|
||||||
|
// keeps the feed copy.
|
||||||
|
func TestRunSteps_feedEchoIsNotRecordedAsEnhanced(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
catPrompt := aiprompts.SplitLegacyCombinedEnhancePrompt(a1StolckiLegacyPrompt)
|
||||||
|
echo, _ := json.Marshal(map[string]any{"name": a1FeedName, "description": a1FeedDesc})
|
||||||
|
c := &formulaRegressionCompleter{reply: string(echo)}
|
||||||
|
in := a1ProductInput(t, catPrompt,
|
||||||
|
jsonObject(t, a1BrandOnlyTitleTemplate), jsonObject(t, a1GenericEnglishDescTemplate))
|
||||||
|
|
||||||
|
out, err := (&Engine{Completer: c}).RunSteps(
|
||||||
|
context.Background(), "co", in, "enhance", []string{"stolcki"}, StepPolicy{AllowAI: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c.calls < 2 {
|
||||||
|
t.Fatalf("feed echo must trigger the formula/title retry, calls=%d", c.calls)
|
||||||
|
}
|
||||||
|
if h, _ := out.FieldSources[FieldEnhanceInputHash].(string); h != "" {
|
||||||
|
t.Fatalf("feed echo must not persist an enhance hash, got %q", h)
|
||||||
|
}
|
||||||
|
if out.Description != a1FeedDesc {
|
||||||
|
t.Fatalf("Original description must stay supplier copy, got %q", out.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original mirrors the feed. A supplier description that trips a quality heuristic
|
||||||
|
// must still show up as Original rather than being blanked or replaced with the
|
||||||
|
// enriched text (that replacement is what Review reported as "Matched").
|
||||||
|
func TestRunSteps_originalNeverBorrowsEnrichedCopy(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
weakFeedDesc := "Ta stol je izdelek znamke BRUNNER."
|
||||||
|
if !company.LooksLikeHeuristicSynthesize(weakFeedDesc) {
|
||||||
|
t.Fatalf("fixture should trip the invent heuristic: %q", weakFeedDesc)
|
||||||
|
}
|
||||||
|
catPrompt := aiprompts.SplitLegacyCombinedEnhancePrompt(a1StolckiLegacyPrompt)
|
||||||
|
in := a1ProductInput(t, catPrompt,
|
||||||
|
jsonObject(t, a1BrandOnlyTitleTemplate), jsonObject(t, a1GenericEnglishDescTemplate))
|
||||||
|
in.Description = weakFeedDesc
|
||||||
|
in.Mapped["description"] = weakFeedDesc
|
||||||
|
|
||||||
|
c := &formulaRegressionCompleter{reply: a1FormulaReply}
|
||||||
|
out, err := (&Engine{Completer: c}).RunSteps(
|
||||||
|
context.Background(), "co", in, "enhance", []string{"stolcki"}, StepPolicy{AllowAI: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if out.Description != weakFeedDesc {
|
||||||
|
t.Fatalf("Original must stay the supplier text, got %q", out.Description)
|
||||||
|
}
|
||||||
|
if out.ProcessedDescription == out.Description {
|
||||||
|
t.Fatal("enriched must differ from Original")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A brand-only title_template ({elements:[brand]}) is a migration stub, not a
|
||||||
|
// formula: instructing "name = brand" produces a title the picker then discards in
|
||||||
|
// favour of the feed name — an LLM call spent to reproduce the supplier title.
|
||||||
|
func TestFormatTitleFormulaConstraint_ignoresBrandOnlyStub(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := FormatTitleFormulaConstraint(jsonObject(t, a1BrandOnlyTitleTemplate)); got != "" {
|
||||||
|
t.Fatalf("brand-only stub must not read as a Title formula, got:\n%s", got)
|
||||||
|
}
|
||||||
|
real := map[string]any{"separator": " ", "elements": []any{
|
||||||
|
map[string]any{"type": "variable", "value": "product_type"},
|
||||||
|
map[string]any{"type": "variable", "value": "brand"},
|
||||||
|
map[string]any{"type": "variable", "value": "product_model"},
|
||||||
|
}}
|
||||||
|
if FormatTitleFormulaConstraint(real) == "" {
|
||||||
|
t.Fatal("a real multi-slot title formula must still produce constraints")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A formula name that is shorter than — and a prefix of — the supplier name must
|
||||||
|
// survive: preferredProductTitle's brand-stub rule would otherwise hand the feed
|
||||||
|
// title back and undo the Title formula.
|
||||||
|
func TestFormulaAwareTitle_keepsShortFormulaNameOverSupplierPrefix(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
in := ProductInput{
|
||||||
|
Name: "BRUNNER stol za kampiranje ONE SHOT sivo črn 0404164N.C20",
|
||||||
|
CategoryEnhancePrompt: aiprompts.SplitLegacyCombinedEnhancePrompt(a1StolckiLegacyPrompt),
|
||||||
|
}
|
||||||
|
if !categoryRequiresTitleRewrite(in) {
|
||||||
|
t.Fatal("A1 Title section must count as a rewrite formula")
|
||||||
|
}
|
||||||
|
if got := formulaAwareTitle(in, "BRUNNER stol"); got != "BRUNNER stol" {
|
||||||
|
t.Fatalf("formula name dropped for the supplier title: %q", got)
|
||||||
|
}
|
||||||
|
// Without a category rewrite rule the richer supplier title still wins.
|
||||||
|
plain := ProductInput{Name: in.Name}
|
||||||
|
if got := formulaAwareTitle(plain, "BRUNNER stol"); got != in.Name {
|
||||||
|
t.Fatalf("without a formula the fuller supplier title should win, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,36 @@ func TestHashEnhanceInput_stableAndSensitive(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enhanceHashFor runs the real pipeline once with a compliant stub reply and
|
||||||
|
// returns the enhance_input_hash it stamped. Deriving it this way keeps hash-skip
|
||||||
|
// tests correct as the steps feeding enhance (parsed specs, filled fields,
|
||||||
|
// category) evolve — hard-coding HashEnhanceInput args silently drifts instead.
|
||||||
|
func enhanceHashFor(t *testing.T, in ProductInput, processingType string) string {
|
||||||
|
t.Helper()
|
||||||
|
e := &Engine{
|
||||||
|
Completer: stubCompleter{fn: func(string, string) (Completion, error) {
|
||||||
|
return Completion{
|
||||||
|
Text: `{"name":"Seed Title","description":"Seed retail copy with enough factual detail to pass the enhance quality gate."}`,
|
||||||
|
TotalTokens: 1,
|
||||||
|
}, nil
|
||||||
|
}},
|
||||||
|
Vector: NoopVectorCategorizer{},
|
||||||
|
}
|
||||||
|
seed := in
|
||||||
|
seed.PriorEnhanceHash = ""
|
||||||
|
seed.PriorProcessedName = ""
|
||||||
|
seed.PriorProcessedDescription = ""
|
||||||
|
out, err := e.RunSteps(context.Background(), "co", seed, processingType, nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
hash, _ := out.FieldSources[FieldEnhanceInputHash].(string)
|
||||||
|
if hash == "" {
|
||||||
|
t.Fatalf("seed run did not stamp an enhance hash: sources=%v notes=%v", out.FieldSources, out.Notes)
|
||||||
|
}
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunSteps_skipsEnhanceWhenInputHashUnchanged(t *testing.T) {
|
func TestRunSteps_skipsEnhanceWhenInputHashUnchanged(t *testing.T) {
|
||||||
calls := 0
|
calls := 0
|
||||||
e := &Engine{
|
e := &Engine{
|
||||||
@@ -51,13 +81,7 @@ func TestRunSteps_skipsEnhanceWhenInputHashUnchanged(t *testing.T) {
|
|||||||
PriorProcessedName: "Cached Widget",
|
PriorProcessedName: "Cached Widget",
|
||||||
PriorProcessedDescription: priorDesc,
|
PriorProcessedDescription: priorDesc,
|
||||||
}
|
}
|
||||||
// First pass without prior hash to compute the hash shape via enhance path is awkward;
|
in.PriorEnhanceHash = enhanceHashFor(t, in, "enhance_only")
|
||||||
// compute the same hash RunSteps will see after normalize (name/desc from mapped).
|
|
||||||
// enhance_only: normalize then enhance with out.Name from normalized.
|
|
||||||
normName := "Widget"
|
|
||||||
normDesc := "A widget with enough mapped detail for hashing."
|
|
||||||
sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{})
|
|
||||||
in.PriorEnhanceHash = HashEnhanceInput("", normName, normDesc, "", "", sysTpl, userTpl, map[string]any{})
|
|
||||||
|
|
||||||
out, err := e.RunSteps(context.Background(), "co", in, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
out, err := e.RunSteps(context.Background(), "co", in, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -381,16 +405,15 @@ func TestRunSteps_synthPriorHashDoesNotSkipReprocess(t *testing.T) {
|
|||||||
if !company.LooksLikeHeuristicSynthesize(synthPrior) {
|
if !company.LooksLikeHeuristicSynthesize(synthPrior) {
|
||||||
t.Fatalf("expected invent synth prior, got %q", synthPrior)
|
t.Fatalf("expected invent synth prior, got %q", synthPrior)
|
||||||
}
|
}
|
||||||
sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{})
|
base := ProductInput{
|
||||||
priorHash := HashEnhanceInput("", normName, normDesc, "", "", sysTpl, userTpl, map[string]any{})
|
Name: normName,
|
||||||
out, err := e.RunSteps(context.Background(), "co", ProductInput{
|
Description: normDesc,
|
||||||
Name: normName,
|
Mapped: map[string]any{"name": normName, "description": normDesc},
|
||||||
Description: normDesc,
|
}
|
||||||
Mapped: map[string]any{"name": normName, "description": normDesc},
|
base.PriorEnhanceHash = enhanceHashFor(t, base, "enhance_only")
|
||||||
PriorEnhanceHash: priorHash,
|
base.PriorProcessedName = normName
|
||||||
PriorProcessedName: normName,
|
base.PriorProcessedDescription = synthPrior
|
||||||
PriorProcessedDescription: synthPrior,
|
out, err := e.RunSteps(context.Background(), "co", base, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
||||||
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,16 @@ func titleFormulaAttrKeys(template any) []string {
|
|||||||
|
|
||||||
// FormatTitleFormulaConstraint turns categories.title_template into enhance-prompt
|
// FormatTitleFormulaConstraint turns categories.title_template into enhance-prompt
|
||||||
// constraints (element order: literal text + attribute variable keys).
|
// constraints (element order: literal text + attribute variable keys).
|
||||||
|
//
|
||||||
|
// Brand-only stubs ({elements:[brand]}, the shape an older A1 migration wrote onto
|
||||||
|
// every category) are not a formula: instructing "name = brand" yields a
|
||||||
|
// brand-only title that the title picker then rejects in favour of the supplier
|
||||||
|
// name — an LLM call spent to reproduce the feed. Treat them as absent so the
|
||||||
|
// category prompt / built-in retail rules drive the name instead.
|
||||||
func FormatTitleFormulaConstraint(template any) string {
|
func FormatTitleFormulaConstraint(template any) string {
|
||||||
|
if aiprompts.TitleTemplateIsBrandOnly(template) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
elements, separator, ok := parseTitleFormula(template)
|
elements, separator, ok := parseTitleFormula(template)
|
||||||
if !ok || len(elements) == 0 {
|
if !ok || len(elements) == 0 {
|
||||||
return ""
|
return ""
|
||||||
@@ -521,6 +530,31 @@ func categoryFormulasFor(in ProductInput, category string) (title, description a
|
|||||||
return f.TitleTemplate, f.DescriptionTemplate
|
return f.TitleTemplate, f.DescriptionTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// effectiveDescriptionTemplateFor resolves the description formula RunSteps should
|
||||||
|
// judge a category's copy against: the category's own template, else one derived
|
||||||
|
// from its prompt, else the platform default. Mirrors what e.enhance renders, so
|
||||||
|
// the final repair pass cannot demand a different formula than the one the model
|
||||||
|
// was given.
|
||||||
|
func effectiveDescriptionTemplateFor(in ProductInput, out StepResult) any {
|
||||||
|
category := strings.TrimSpace(out.Category)
|
||||||
|
_, descTpl := categoryFormulasFor(in, category)
|
||||||
|
catPrompt := strings.TrimSpace(in.CategoryEnhancePrompt)
|
||||||
|
if catPrompt == "" {
|
||||||
|
catPrompt = categoryEnhancePromptFor(in.CategoryPromptsByLang, category, in.Language, in.Language)
|
||||||
|
}
|
||||||
|
if effective := aiprompts.EffectiveDescriptionTemplateAny(descTpl, catPrompt); effective != nil {
|
||||||
|
return effective
|
||||||
|
}
|
||||||
|
if catPrompt != "" || descTpl != nil {
|
||||||
|
return descTpl
|
||||||
|
}
|
||||||
|
probe := ProductInput{
|
||||||
|
CategoryUniqueID: category,
|
||||||
|
CategoryDisplayName: categoryDisplayLabel(out),
|
||||||
|
}
|
||||||
|
return withDefaultCategoryFormula(probe).DescriptionTemplate
|
||||||
|
}
|
||||||
|
|
||||||
func asObjectMap(v any) (map[string]any, error) {
|
func asObjectMap(v any) (map[string]any, error) {
|
||||||
switch t := v.(type) {
|
switch t := v.(type) {
|
||||||
case map[string]any:
|
case map[string]any:
|
||||||
|
|||||||
@@ -65,12 +65,13 @@ func TestAppendDescriptionFormulaSystemOverride(t *testing.T) {
|
|||||||
EnhanceUserTemplate: "Name: {{name}}",
|
EnhanceUserTemplate: "Name: {{name}}",
|
||||||
DescriptionTemplate: desc,
|
DescriptionTemplate: desc,
|
||||||
})
|
})
|
||||||
// Structured description_template uses A1 GPT-predloga path (legacy generator.php).
|
// Structured description_template uses the A1 template path (legacy generator.php).
|
||||||
if !strings.Contains(user, "GPT predloga:") || !strings.Contains(user, "<H1>{Heading}</H1>") {
|
// Fixture instructions are English, so the scaffolding renders in English.
|
||||||
t.Fatalf("missing A1 description predloga: %s", user)
|
if !strings.Contains(user, "Product template:") || !strings.Contains(user, "<H1>{Heading}</H1>") {
|
||||||
|
t.Fatalf("missing description template: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(sys, "Si tekstopisec") {
|
if !strings.Contains(sys, "You are a copywriter") {
|
||||||
t.Fatalf("system should be A1 copywriter when formula set: %s", sys)
|
t.Fatalf("system should be the formula copywriter when formula set: %s", sys)
|
||||||
}
|
}
|
||||||
if AppendDescriptionFormulaSystemOverride("plain", nil) != "plain" {
|
if AppendDescriptionFormulaSystemOverride("plain", nil) != "plain" {
|
||||||
t.Fatal("empty formula must be no-op")
|
t.Fatal("empty formula must be no-op")
|
||||||
@@ -91,14 +92,14 @@ func TestAppendTitleFormulaSystemOverride(t *testing.T) {
|
|||||||
EnhanceUserTemplate: "Name: {{name}}",
|
EnhanceUserTemplate: "Name: {{name}}",
|
||||||
TitleTemplate: title,
|
TitleTemplate: title,
|
||||||
})
|
})
|
||||||
if !strings.Contains(user, "GPT predloga:") || !strings.Contains(user, "<name>{") {
|
if !strings.Contains(user, "Product template:") || !strings.Contains(user, "<name>{") {
|
||||||
t.Fatalf("missing A1 title predloga: %s", user)
|
t.Fatalf("missing title template: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(user, "brand") {
|
if !strings.Contains(user, "brand") {
|
||||||
t.Fatalf("missing brand in title formula: %s", user)
|
t.Fatalf("missing brand in title formula: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(sys, "Si tekstopisec") {
|
if !strings.Contains(sys, "You are a copywriter") {
|
||||||
t.Fatalf("system should be A1 copywriter when title formula set: %s", sys)
|
t.Fatalf("system should be the formula copywriter when title formula set: %s", sys)
|
||||||
}
|
}
|
||||||
if AppendTitleFormulaSystemOverride("plain", nil) != "plain" {
|
if AppendTitleFormulaSystemOverride("plain", nil) != "plain" {
|
||||||
t.Fatal("empty title formula must be no-op")
|
t.Fatal("empty title formula must be no-op")
|
||||||
@@ -246,8 +247,8 @@ func TestAppendFormulaConstraints_injectedIntoResolve(t *testing.T) {
|
|||||||
TitleTemplate: title,
|
TitleTemplate: title,
|
||||||
DescriptionTemplate: desc,
|
DescriptionTemplate: desc,
|
||||||
})
|
})
|
||||||
if !strings.Contains(user, "GPT predloga:") {
|
if !strings.Contains(user, "Product template:") {
|
||||||
t.Fatalf("expected A1 GPT predloga: %s", user)
|
t.Fatalf("expected formula template: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(user, "<name>{") || !strings.Contains(user, "brand") {
|
if !strings.Contains(user, "<name>{") || !strings.Contains(user, "brand") {
|
||||||
t.Fatalf("missing title formula in predloga: %s", user)
|
t.Fatalf("missing title formula in predloga: %s", user)
|
||||||
@@ -258,8 +259,8 @@ func TestAppendFormulaConstraints_injectedIntoResolve(t *testing.T) {
|
|||||||
if !strings.Contains(user, "<metaDescription>{") || !strings.Contains(user, "120-155") {
|
if !strings.Contains(user, "<metaDescription>{") || !strings.Contains(user, "120-155") {
|
||||||
t.Fatalf("missing SEO meta in predloga: %s", user)
|
t.Fatalf("missing SEO meta in predloga: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(sys, "Si tekstopisec") {
|
if !strings.Contains(sys, "You are a copywriter") {
|
||||||
t.Fatalf("system missing A1 copywriter: %s", sys)
|
t.Fatalf("system missing formula copywriter: %s", sys)
|
||||||
}
|
}
|
||||||
// Render substitutes {{language}} in formula blocks.
|
// Render substitutes {{language}} in formula blocks.
|
||||||
_, rendered := RenderProductEnhancePrompts(
|
_, rendered := RenderProductEnhancePrompts(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string) {
|
func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string) {
|
||||||
|
in = withDefaultCategoryFormula(in)
|
||||||
systemTpl = strings.TrimSpace(in.EnhanceSystemTemplate)
|
systemTpl = strings.TrimSpace(in.EnhanceSystemTemplate)
|
||||||
userTpl = strings.TrimSpace(in.EnhanceUserTemplate)
|
userTpl = strings.TrimSpace(in.EnhanceUserTemplate)
|
||||||
catPrompt := strings.TrimSpace(in.CategoryEnhancePrompt)
|
catPrompt := strings.TrimSpace(in.CategoryEnhancePrompt)
|
||||||
@@ -17,8 +18,12 @@ func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string)
|
|||||||
// <name>/<metaDescription>/HTML). When formulas are set, rebuild that shape
|
// <name>/<metaDescription>/HTML). When formulas are set, rebuild that shape
|
||||||
// instead of burying instructions under the generic retail copywriter overlay.
|
// instead of burying instructions under the generic retail copywriter overlay.
|
||||||
if categoryUsesA1StyleFormula(catPrompt, in.TitleTemplate, descTpl) {
|
if categoryUsesA1StyleFormula(catPrompt, in.TitleTemplate, descTpl) {
|
||||||
userTpl = buildA1StyleEnhanceUserTemplate(catPrompt, in.TitleTemplate, descTpl, in.OmitSEOMeta)
|
var slovenianFormula bool
|
||||||
systemTpl = a1StyleEnhanceSystemTemplate
|
userTpl, slovenianFormula = buildA1StyleEnhanceUserTemplate(catPrompt, in.TitleTemplate, descTpl, in.OmitSEOMeta)
|
||||||
|
systemTpl = a1StyleEnhanceSystemTemplateEN
|
||||||
|
if slovenianFormula {
|
||||||
|
systemTpl = a1StyleEnhanceSystemTemplateSL
|
||||||
|
}
|
||||||
allowed := enhanceAllowedAttrKeys(in, in.CategoryUniqueID)
|
allowed := enhanceAllowedAttrKeys(in, in.CategoryUniqueID)
|
||||||
userTpl = AppendAttributeConstraints(userTpl, allowed, in.TitleTemplate)
|
userTpl = AppendAttributeConstraints(userTpl, allowed, in.TitleTemplate)
|
||||||
return systemTpl, userTpl
|
return systemTpl, userTpl
|
||||||
@@ -59,6 +64,47 @@ func resolveProductPromptTemplates(in ProductInput) (systemTpl, userTpl string)
|
|||||||
return systemTpl, userTpl
|
return systemTpl, userTpl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withDefaultCategoryFormula supplies the platform-default formula when the
|
||||||
|
// category carries none of its own.
|
||||||
|
//
|
||||||
|
// Categories created before defaults existed (and any tenant who never opened the
|
||||||
|
// formula editor) would otherwise enhance through the generic "1-3 factual
|
||||||
|
// paragraphs" rule and produce copy shaped by the feed rather than by a formula.
|
||||||
|
// The default is the English translation of the A1 template style; a category that
|
||||||
|
// has its own prompt or formula — every A1 category — is returned untouched.
|
||||||
|
func withDefaultCategoryFormula(in ProductInput) ProductInput {
|
||||||
|
// Only categorised products: with no category there is nothing to look a
|
||||||
|
// default up for, and the company/built-in enhance template still applies.
|
||||||
|
if strings.TrimSpace(in.CategoryUniqueID) == "" && strings.TrimSpace(in.CategoryDisplayName) == "" {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
// A tenant-authored enhance template is an explicit choice and outranks any
|
||||||
|
// platform default.
|
||||||
|
if !aiprompts.IsBuiltInProductEnhanceUserTemplate(in.EnhanceUserTemplate) {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(in.CategoryEnhancePrompt) != "" {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
if FormatTitleFormulaConstraint(in.TitleTemplate) != "" ||
|
||||||
|
FormatDescriptionFormulaConstraint(in.DescriptionTemplate) != "" ||
|
||||||
|
FormatMetaFormulaConstraint(in.DescriptionTemplate) != "" {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
def := aiprompts.DefaultCategoryFormula(in.CategoryUniqueID, in.CategoryDisplayName)
|
||||||
|
if def.Overlay == "" {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
in.CategoryEnhancePrompt = def.Overlay
|
||||||
|
if in.TitleTemplate == nil && def.TitleTemplateJSON != "" {
|
||||||
|
in.TitleTemplate = decodeOptionalJSONObject([]byte(def.TitleTemplateJSON))
|
||||||
|
}
|
||||||
|
if in.DescriptionTemplate == nil && def.DescriptionTemplateJSON != "" {
|
||||||
|
in.DescriptionTemplate = decodeOptionalJSONObject([]byte(def.DescriptionTemplateJSON))
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
// thinEnhanceUserInstruction is prepended when Desc is empty or ≈ Name so the model
|
// thinEnhanceUserInstruction is prepended when Desc is empty or ≈ Name so the model
|
||||||
// builds copy from Attrs+Category instead of echoing the title.
|
// builds copy from Attrs+Category instead of echoing the title.
|
||||||
const thinEnhanceUserInstruction = "Build the description from Attrs and Category only; never copy Name."
|
const thinEnhanceUserInstruction = "Build the description from Attrs and Category only; never copy Name."
|
||||||
|
|||||||
@@ -82,12 +82,13 @@ func TestResolveProductPromptTemplates_categoryWithTitleFormula(t *testing.T) {
|
|||||||
TitleTemplate: title,
|
TitleTemplate: title,
|
||||||
DescriptionTemplate: desc,
|
DescriptionTemplate: desc,
|
||||||
})
|
})
|
||||||
// Structured formulas trigger A1-style GPT predloga (legacy generator.php behavior).
|
// Structured formulas trigger the A1-style template (legacy generator.php behavior);
|
||||||
if !strings.Contains(sys, "Si tekstopisec") {
|
// this fixture's instructions are English so the scaffolding renders in English.
|
||||||
t.Fatalf("expected A1 system when formulas set: %s", sys)
|
if !strings.Contains(sys, "You are a copywriter") {
|
||||||
|
t.Fatalf("expected formula system when formulas set: %s", sys)
|
||||||
}
|
}
|
||||||
if !strings.Contains(user, "GPT predloga:") {
|
if !strings.Contains(user, "Product template:") {
|
||||||
t.Fatalf("expected GPT predloga user: %s", user)
|
t.Fatalf("expected formula template user: %s", user)
|
||||||
}
|
}
|
||||||
if !strings.Contains(user, "Emphasize energy class") && !strings.Contains(user, "<name>") {
|
if !strings.Contains(user, "Emphasize energy class") && !strings.Contains(user, "<name>") {
|
||||||
t.Fatalf("expected title/desc formula in predloga: %s", user)
|
t.Fatalf("expected title/desc formula in predloga: %s", user)
|
||||||
|
|||||||
@@ -48,10 +48,12 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
|||||||
in.Name,
|
in.Name,
|
||||||
in.PriorProcessedName,
|
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"]),
|
stringFromAny(normalized["description"]),
|
||||||
in.Description,
|
in.Description,
|
||||||
in.PriorProcessedDescription,
|
|
||||||
)
|
)
|
||||||
// mapped_data.category / category_unique_id (unique_id codes) win.
|
// mapped_data.category / category_unique_id (unique_id codes) win.
|
||||||
applyCategoryFromMapped(&out, normalized, in.Mapped, in.Raw)
|
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.Name,
|
||||||
in.PriorProcessedName,
|
in.PriorProcessedName,
|
||||||
)
|
)
|
||||||
out.Description = preferredProductDescription(out.Name,
|
out.Description = supplierOriginalDescription(out.Name,
|
||||||
stringFromAny(normalized["description"]),
|
stringFromAny(normalized["description"]),
|
||||||
out.Description,
|
out.Description,
|
||||||
in.Description,
|
in.Description,
|
||||||
@@ -451,7 +453,7 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
|||||||
out.ProcessedDescription = out.Description
|
out.ProcessedDescription = out.Description
|
||||||
}
|
}
|
||||||
// Timeout/error/empty/formula-miss: always synthesize a factual fallback when a title exists.
|
// 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 out.ProcessedName != "" && descriptionNeedsEnhanceRepair(out.ProcessedDescription, failDescTpl, out.ProcessedName, out.Name) {
|
||||||
if synth := synthesizeProductDescription(out.ProcessedName, displayCat, primary, enhanceAttrs, failDescTpl); synth != "" {
|
if synth := synthesizeProductDescription(out.ProcessedName, displayCat, primary, enhanceAttrs, failDescTpl); synth != "" {
|
||||||
out.ProcessedDescription = 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
|
// Pipelines without StepCategorize (normalize_only) still run vector + LLM
|
||||||
// vector + LLM categorize when category is empty so enhance is not fed a blank.
|
// categorize when category is empty so downstream steps are not fed a blank.
|
||||||
if !stepsContain(steps, StepCategorize) {
|
if !stepsContain(steps, StepCategorize) {
|
||||||
runCategorizeStep(ctx, e, companyID, &out, in, categoryNames, policy)
|
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)
|
preserveCategoryIfEmpty(&out, in.PriorCategory)
|
||||||
syncCategoryName(&out, in.CategoryNamesByUID)
|
syncCategoryName(&out, in.CategoryNamesByUID)
|
||||||
out.Name = preferredProductTitle(in.GTIN, out.Name, out.ProcessedName, in.Name, in.PriorProcessedName)
|
// Original stays supplier copy; only the enriched side may borrow from it.
|
||||||
out.ProcessedName = preferredProductTitle(in.GTIN, out.ProcessedName, out.Name, in.PriorProcessedName, in.Name)
|
// Filling Original from ProcessedName/Description is what produced Review rows
|
||||||
out.Description = preferredProductDescription(out.Name, out.Description, out.ProcessedDescription, in.Description)
|
// 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)
|
out.ProcessedDescription = preferredProductDescription(out.ProcessedName, out.ProcessedDescription, out.Description, in.PriorProcessedDescription)
|
||||||
if out.ProcessedName == "" {
|
if out.ProcessedName == "" {
|
||||||
out.ProcessedName = out.Name
|
out.ProcessedName = out.Name
|
||||||
@@ -549,7 +556,7 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
|||||||
displayCat := categoryDisplayLabel(out)
|
displayCat := categoryDisplayLabel(out)
|
||||||
// After vector/mapped category resolution, formulas may key for the first time —
|
// After vector/mapped category resolution, formulas may key for the first time —
|
||||||
// repair short prose / title echo that ignored A1 description_template.
|
// repair short prose / title echo that ignored A1 description_template.
|
||||||
_, finalDescTpl := categoryFormulasFor(in, out.Category)
|
finalDescTpl := effectiveDescriptionTemplateFor(in, out)
|
||||||
finalRepaired := false
|
finalRepaired := false
|
||||||
if descriptionNeedsEnhanceRepair(out.ProcessedDescription, finalDescTpl, out.ProcessedName, out.Name) {
|
if descriptionNeedsEnhanceRepair(out.ProcessedDescription, finalDescTpl, out.ProcessedName, out.Name) {
|
||||||
if synth := synthesizeProductDescription(out.ProcessedName, displayCat, in.Language, out.Attributes, finalDescTpl); synth != "" {
|
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 {
|
func resolveSteps(processingType string) []string {
|
||||||
switch strings.ToLower(strings.TrimSpace(processingType)) {
|
switch strings.ToLower(strings.TrimSpace(processingType)) {
|
||||||
case "enhance", "enhance_only", "enhance-only", "title", "description":
|
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":
|
case "attributes", "attributes_only", "specs", "specifications":
|
||||||
return []string{StepNormalize, StepParseSpecs, StepFillFields}
|
return []string{StepNormalize, StepParseSpecs, StepFillFields}
|
||||||
case "eprel", "eprel_only":
|
case "eprel", "eprel_only":
|
||||||
@@ -779,10 +791,10 @@ func normalizeTitleCompare(s string) string {
|
|||||||
return strings.TrimSpace(b.String())
|
return strings.TrimSpace(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func titleNeedsRewriteRetry(in ProductInput, name string) bool {
|
// categoryRequiresTitleRewrite reports that the category asks enhance for a NEW
|
||||||
if !titlesAreEquivalent(name, in.Name) {
|
// product name — a structured title_template, or free-form Title instructions that
|
||||||
return false
|
// spell out a naming formula (legacy A1 <name>{…}</name>).
|
||||||
}
|
func categoryRequiresTitleRewrite(in ProductInput) bool {
|
||||||
if aiprompts.CategoryTitlePromptRequiresRewrite(in.CategoryEnhancePrompt) {
|
if aiprompts.CategoryTitlePromptRequiresRewrite(in.CategoryEnhancePrompt) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -792,6 +804,62 @@ func titleNeedsRewriteRetry(in ProductInput, name string) bool {
|
|||||||
return FormatTitleSectionConstraint(in.CategoryEnhancePrompt) != ""
|
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
|
// 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 {
|
||||||
@@ -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) {
|
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
|
// Prompt Description HTML overlays count as the formula when description_template
|
||||||
// is empty — otherwise enhance accepts supplier-like prose and skips formula retry.
|
// is empty — otherwise enhance accepts supplier-like prose and skips formula retry.
|
||||||
if effective := aiprompts.EffectiveDescriptionTemplateAny(in.DescriptionTemplate, in.CategoryEnhancePrompt); effective != nil {
|
if effective := aiprompts.EffectiveDescriptionTemplateAny(in.DescriptionTemplate, in.CategoryEnhancePrompt); effective != nil {
|
||||||
in.DescriptionTemplate = effective
|
in.DescriptionTemplate = effective
|
||||||
}
|
}
|
||||||
catUID := strings.TrimSpace(in.CategoryUniqueID)
|
|
||||||
catName := strings.TrimSpace(category)
|
|
||||||
sysTpl, userTpl := resolveProductPromptTemplates(in)
|
sysTpl, userTpl := resolveProductPromptTemplates(in)
|
||||||
hash := HashEnhanceInput(category, in.Name, in.Description, in.BrandPrompt, in.Language, sysTpl, userTpl, attrs)
|
hash := HashEnhanceInput(category, in.Name, in.Description, in.BrandPrompt, in.Language, sysTpl, userTpl, attrs)
|
||||||
if e == nil || e.Completer == nil {
|
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
|
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).
|
// Quality-gate on LLM description alone (do not absorb originals yet).
|
||||||
desc := preferredProductDescription(name, llmDesc)
|
desc := preferredProductDescription(name, llmDesc)
|
||||||
didFormulaRetry := false
|
didFormulaRetry := false
|
||||||
@@ -1582,7 +1656,9 @@ func preferredProductDescription(title string, candidates ...string) string {
|
|||||||
if containsWeakFillerPhrase(c) {
|
if containsWeakFillerPhrase(c) {
|
||||||
continue
|
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
|
continue
|
||||||
}
|
}
|
||||||
return SanitizeOutput(c)
|
return SanitizeOutput(c)
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ func (s stubCompleter) Complete(_ context.Context, system, user string) (Complet
|
|||||||
func TestResolveSteps(t *testing.T) {
|
func TestResolveSteps(t *testing.T) {
|
||||||
cases := map[string][]string{
|
cases := map[string][]string{
|
||||||
"full": {StepNormalize, StepParseSpecs, StepFillFields, StepEPREL, StepCategorize, StepAIEnhance},
|
"full": {StepNormalize, StepParseSpecs, StepFillFields, StepEPREL, StepCategorize, StepAIEnhance},
|
||||||
"enhance_only": {StepNormalize, StepAIEnhance},
|
"enhance_only": {StepNormalize, StepParseSpecs, StepFillFields, StepCategorize, StepAIEnhance},
|
||||||
|
"title": {StepNormalize, StepParseSpecs, StepFillFields, StepCategorize, StepAIEnhance},
|
||||||
|
"description": {StepNormalize, StepParseSpecs, StepFillFields, StepCategorize, StepAIEnhance},
|
||||||
"attributes_only": {StepNormalize, StepParseSpecs, StepFillFields},
|
"attributes_only": {StepNormalize, StepParseSpecs, StepFillFields},
|
||||||
"eprel_only": {StepNormalize, StepParseSpecs, StepEPREL},
|
"eprel_only": {StepNormalize, StepParseSpecs, StepEPREL},
|
||||||
"normalize_only": {StepNormalize},
|
"normalize_only": {StepNormalize},
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Category formulas
|
||||||
|
|
||||||
|
A category formula is what makes enhance *generate* copy instead of reshuffling the
|
||||||
|
feed. The feed supplies facts; the formula supplies the shape.
|
||||||
|
|
||||||
|
Legacy Descrybe (`descrybe-legacy/generator.php`) did this by making the category's
|
||||||
|
`Prompt` column the entire user message — a "GPT predloga" with `<name>{…}</name>`,
|
||||||
|
`<metaDescription>{…}</metaDescription>` and an HTML body of `<H2>`/`<p>` pairs
|
||||||
|
closing with a technical-specification list. v2 keeps that behaviour and adds
|
||||||
|
structured `title_template` / `description_template` columns on top of it.
|
||||||
|
|
||||||
|
## Two formula sets
|
||||||
|
|
||||||
|
| | A1 Slovenija (legacy plan) | Everyone else |
|
||||||
|
|---|---|---|
|
||||||
|
| Source | `scripts/seed/a1-category-prompts.json` (+ `wp_product_categories.sql`) | `apps/api/internal/aiprompts/defaults/default-category-prompts.json` |
|
||||||
|
| Applied by | `cmd/seed-a1`, `cmd/repair-category-prompts`, Admin → Sync A1 | category create + enhance-time fallback |
|
||||||
|
| Language of the formula text | Slovenian (the tenant's own copy) | English |
|
||||||
|
| Prompt scaffolding | Slovenian (`GPT predloga:`, `Staro_ime_izdelka:`) | English (`Product template:`, `Old_product_name:`) |
|
||||||
|
|
||||||
|
Both drive the same code path. The scaffolding language follows the formula's own
|
||||||
|
language (`reSlovenianFormulaCue` in `processing/a1_enhance_prompt.go`) so A1 keeps
|
||||||
|
the exact frame `generator.php` used. Output language is always `{{language}}` from
|
||||||
|
the company's content language — an English formula still produces Slovenian copy
|
||||||
|
for a Slovenian tenant.
|
||||||
|
|
||||||
|
## The English defaults
|
||||||
|
|
||||||
|
`default-category-prompts.json` is the A1 seed translated: 116 categories, same
|
||||||
|
naming formulas and body structure, generated by
|
||||||
|
|
||||||
|
```
|
||||||
|
make seed-defaults # regenerate
|
||||||
|
make check-defaults # fail if stale (CI guard)
|
||||||
|
```
|
||||||
|
|
||||||
|
Translation is phrase-level over a closed vocabulary (55 title slots, 39 body
|
||||||
|
blocks, 1 meta rule) and the generator **fails** if any Slovenian text survives, so
|
||||||
|
a new legacy phrase can never ship untranslated.
|
||||||
|
|
||||||
|
Where they apply:
|
||||||
|
|
||||||
|
1. **Category create** — `catalog.ApplyDefaultCategoryFormulas` fills `prompt`,
|
||||||
|
`title_template` and `description_template` on new categories (API create and
|
||||||
|
CSV taxonomy import). Only empty columns are written, so nothing existing is
|
||||||
|
overwritten.
|
||||||
|
2. **Enhance-time fallback** — `processing.withDefaultCategoryFormula` supplies the
|
||||||
|
default for a categorised product whose category still has no formula. It is
|
||||||
|
skipped when the product has no category, when the category has its own
|
||||||
|
prompt/formula, or when the tenant authored their own enhance template.
|
||||||
|
|
||||||
|
A category outside the seeded taxonomy gets `GenericDefaultCategoryPrompt` — same
|
||||||
|
structure, no category-specific naming slots.
|
||||||
|
|
||||||
|
## Proving it locally
|
||||||
|
|
||||||
|
`cmd/formula-e2e` builds the pipeline the way `cmd/worker` does, runs a real job and
|
||||||
|
checks the output is not a feed copy:
|
||||||
|
|
||||||
|
```
|
||||||
|
make up # Postgres :5433
|
||||||
|
cd apps/api && go run ./cmd/mock-llm & # formula-aware local LLM
|
||||||
|
make formula-e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
`cmd/mock-llm` reads the template out of the prompt and answers in its shape using
|
||||||
|
only the supplied name/description/category/attrs. That proves two things a static
|
||||||
|
stub cannot: the formula reached the model, and a compliant reply survives the
|
||||||
|
pipeline's formula gate.
|
||||||
|
|
||||||
|
Useful flags:
|
||||||
|
|
||||||
|
```
|
||||||
|
go run ./cmd/formula-e2e -company "A1 Slovenija" -gtin 8022068075495 -llm-base http://127.0.0.1:18767/v1
|
||||||
|
go run ./cmd/formula-e2e -new-tenant -llm-base http://127.0.0.1:18767/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
`-llm-base` pins the provider for the run; without it the harness uses whatever
|
||||||
|
`platformsettings.ResolveOpenAI` returns (admin DB setting first, then env).
|
||||||
|
|
||||||
|
## Why enhance can still return feed copy
|
||||||
|
|
||||||
|
In order of how often it bites:
|
||||||
|
|
||||||
|
1. **The product has no category.** No category → no formula. Roughly 83% of the A1
|
||||||
|
catalog has no `mapped_data.category` (see `scripts/seed/README.txt`); those rely
|
||||||
|
on the `categorize` step, which needs a working LLM or Pinecone.
|
||||||
|
2. **The AI provider is unreachable.** Enhance falls back to supplier copy and marks
|
||||||
|
`field_sources.name = ai_enhance_failed`. Check the admin OpenAI setting — it
|
||||||
|
overrides the `OPENAI_*` env values.
|
||||||
|
3. **The plan/credits gate.** `ai_enhance skip reason=entitlement_can_use_ai` or
|
||||||
|
`insufficient credits`.
|
||||||
|
4. **The model ignored the formula.** One retry is issued
|
||||||
|
(`descriptionFormulaRetrySuffix` / `titleRewriteRetrySuffix`); if it still fails,
|
||||||
|
the result is marked `synthesized`/`refused` and no `enhance_input_hash` is
|
||||||
|
stored, so a reprocess tries again rather than caching the bad copy.
|
||||||
|
|
||||||
|
`processing: ai_enhance outcome=… category_uid=… title_formula=… desc_formula=…
|
||||||
|
formula_override=…` in the worker log tells you which of these happened.
|
||||||
@@ -113,3 +113,20 @@ Seed (from apps/api, DATABASE_URL set):
|
|||||||
go run ./cmd/seed-support-kb
|
go run ./cmd/seed-support-kb
|
||||||
go run ./cmd/seed-support-kb -file ../../scripts/seed/support-kb-articles-tech.json
|
go run ./cmd/seed-support-kb -file ../../scripts/seed/support-kb-articles-tech.json
|
||||||
Idempotent ON CONFLICT (slug). Do not reuse product slugs for tech articles.
|
Idempotent ON CONFLICT (slug). Do not reuse product slugs for tech articles.
|
||||||
|
|
||||||
|
Platform-default category formulas (English)
|
||||||
|
--------------------------------------------
|
||||||
|
a1-category-prompts.json stays the A1 tenant's own Slovenian formula set. Every
|
||||||
|
other tenant uses the English translation of the same formulas:
|
||||||
|
|
||||||
|
apps/api/internal/aiprompts/defaults/default-category-prompts.json (embedded)
|
||||||
|
|
||||||
|
Generated from this seed by scripts/seed/build-default-category-prompts.mjs:
|
||||||
|
|
||||||
|
make seed-defaults # regenerate after editing the A1 seed or the phrase table
|
||||||
|
make check-defaults # fail when the embedded copy is stale
|
||||||
|
|
||||||
|
The generator translates a closed vocabulary (title slots, body blocks, meta rule)
|
||||||
|
and refuses to emit an entry with any untranslated Slovenian left in it. New
|
||||||
|
categories get these formulas at create time; categories with none fall back to
|
||||||
|
them at enhance time. See docs/category-formulas.md.
|
||||||
|
|||||||
@@ -0,0 +1,372 @@
|
|||||||
|
// Builds scripts/seed/default-category-prompts.json — the English platform-default
|
||||||
|
// category formulas — from the Slovenian A1 legacy seed (a1-category-prompts.json).
|
||||||
|
//
|
||||||
|
// The A1 tenant keeps its own Slovenian prompts (that seed is untouched). Every
|
||||||
|
// other tenant gets the same formula STYLE in English: a <name> naming formula, a
|
||||||
|
// <metaDescription> rule, and an HTML body of H2/paragraph pairs plus a technical
|
||||||
|
// specification list.
|
||||||
|
//
|
||||||
|
// Translation is phrase-level rather than per-entry: the 116 legacy prompts are
|
||||||
|
// built from a small closed vocabulary (55 title slots, 39 body blocks, 1 meta
|
||||||
|
// rule). Every phrase must be covered — the script fails if any Slovenian residue
|
||||||
|
// survives, so a new legacy phrase can never be silently emitted untranslated.
|
||||||
|
//
|
||||||
|
// node scripts/seed/build-default-category-prompts.mjs [--check]
|
||||||
|
//
|
||||||
|
// --check verifies the committed JSON matches what this script produces (CI guard).
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const SOURCE = join(here, "a1-category-prompts.json");
|
||||||
|
// Written into the Go module so aiprompts can go:embed it — the platform default
|
||||||
|
// must ship with the binary, unlike the A1 seed which is read from disk at seed time.
|
||||||
|
const TARGET = join(here, "..", "..", "apps", "api", "internal", "aiprompts", "defaults", "default-category-prompts.json");
|
||||||
|
|
||||||
|
// Category display names. Keys stay the legacy Slovenian names so the generated
|
||||||
|
// entry can carry both as match aliases (a tenant may use either spelling).
|
||||||
|
const CATEGORY_NAMES = {
|
||||||
|
"Agregati": "Generators",
|
||||||
|
"Avto oprema": "Car accessories",
|
||||||
|
"Avto polnilniki": "Car chargers",
|
||||||
|
"Avtosedeži in lupinice": "Car seats and infant carriers",
|
||||||
|
"Bazeni": "Pools",
|
||||||
|
"Bluetooth zvočniki": "Bluetooth speakers",
|
||||||
|
"Brezžični polnilniki": "Wireless chargers",
|
||||||
|
"Brivniki": "Shavers",
|
||||||
|
"Cvrtniki": "Air fryers",
|
||||||
|
"Depilatorji": "Epilators",
|
||||||
|
"Digitalna peresa": "Digital pens",
|
||||||
|
"Dodatki za dom": "Home accessories",
|
||||||
|
"Dodatki za televizorje": "TV accessories",
|
||||||
|
"Dodatki za ure": "Watch accessories",
|
||||||
|
"Dodatna oprema": "Additional equipment",
|
||||||
|
"Dodatna oprema za računalništvo": "Computer accessories",
|
||||||
|
"Droni": "Drones",
|
||||||
|
"Družabne igre": "Board games",
|
||||||
|
"Električna kolesa": "Electric bikes",
|
||||||
|
"Električne zobne ščetke": "Electric toothbrushes",
|
||||||
|
"Električni skiroji": "Electric scooters",
|
||||||
|
"Fitnes oprema": "Fitness equipment",
|
||||||
|
"Gaming monitorji": "Gaming monitors",
|
||||||
|
"Gaming pohištvo": "Gaming furniture",
|
||||||
|
"Gaming prenosniki računalniki": "Gaming laptops",
|
||||||
|
"Gaming stacionarni računalniki": "Gaming desktops",
|
||||||
|
"Giljotine in rezalniki": "Guillotines and trimmers",
|
||||||
|
"Grelniki vode": "Water heaters",
|
||||||
|
"Hišni polnilniki": "Wall chargers",
|
||||||
|
"Hladilniki": "Refrigerators",
|
||||||
|
"Igralne konzole in igre": "Game consoles and games",
|
||||||
|
"Igrače na daljinsko vodenje": "Remote-controlled toys",
|
||||||
|
"Igrače za najmlajše": "Toys for infants",
|
||||||
|
"Kabli": "Cables",
|
||||||
|
"Kamere in fotoaparati": "Cameras",
|
||||||
|
"Kamp oprema in aktivnosti na prostem": "Camping and outdoor equipment",
|
||||||
|
"Kavni aparati": "Coffee machines",
|
||||||
|
"Klasična kolesa": "Classic bikes",
|
||||||
|
"Klimatske naprave": "Air conditioners",
|
||||||
|
"Kuhalne plošče": "Hobs",
|
||||||
|
"Kuhinjske nape": "Cooker hoods",
|
||||||
|
"Kuhinjski roboti": "Food processors",
|
||||||
|
"Likalniki": "Irons",
|
||||||
|
"Masažni bazeni": "Hot tubs",
|
||||||
|
"Mešalniki in blenderji": "Mixers and blenders",
|
||||||
|
"Mikrovalovne pečice": "Microwave ovens",
|
||||||
|
"Miške": "Mice",
|
||||||
|
"Mobilne naprave": "Mobile devices",
|
||||||
|
"Monitorji": "Monitors",
|
||||||
|
"Mrežna oprema": "Networking equipment",
|
||||||
|
"Nadzorne kamere": "Security cameras",
|
||||||
|
"Namizni računalniki": "Desktop computers",
|
||||||
|
"Namizni žari in opekači": "Tabletop grills and toasters",
|
||||||
|
"Naprave za ogrevanje": "Heaters",
|
||||||
|
"Nosilci za TV": "TV mounts",
|
||||||
|
"Oblikovalci in sušilniki za lase": "Hair stylers and dryers",
|
||||||
|
"Obnovljeni prenosniki": "Refurbished laptops",
|
||||||
|
"Obnovljeni telefoni": "Refurbished phones",
|
||||||
|
"Orodje za mojstre": "Power tools",
|
||||||
|
"Osebna nega": "Personal care",
|
||||||
|
"Ostali aparati": "Other appliances",
|
||||||
|
"Ostali dodatki": "Other accessories",
|
||||||
|
"Ostali gaming dodatki": "Other gaming accessories",
|
||||||
|
"Otroška vozila": "Ride-on toys",
|
||||||
|
"Otroške pametne ure": "Kids smartwatches",
|
||||||
|
"Ovitki in mape": "Cases and folios",
|
||||||
|
"Ovitki in torbe za prenosnike": "Laptop cases and bags",
|
||||||
|
"Pametne ure": "Smartwatches",
|
||||||
|
"Pametne čelade": "Smart helmets",
|
||||||
|
"Parni čistilci": "Steam cleaners",
|
||||||
|
"Pečice": "Ovens",
|
||||||
|
"Plastifikatorji": "Laminators",
|
||||||
|
"Poganjalčki": "Balance bikes",
|
||||||
|
"Pomivalni stroji": "Dishwashers",
|
||||||
|
"Posode za kuhanje": "Cookware",
|
||||||
|
"Postelje": "Beds",
|
||||||
|
"Potrošni material": "Consumables",
|
||||||
|
"Pralni stroji": "Washing machines",
|
||||||
|
"Pralno - sušilni stroji": "Washer dryers",
|
||||||
|
"Prenosne baterije": "Power banks",
|
||||||
|
"Prenosni računalniki": "Laptops",
|
||||||
|
"Projektorji": "Projectors",
|
||||||
|
"Računalniško pohištvo": "Computer furniture",
|
||||||
|
"Robotski sesalniki": "Robot vacuums",
|
||||||
|
"Senzorji": "Sensors",
|
||||||
|
"Sesalniki": "Vacuum cleaners",
|
||||||
|
"Skiroji in rolke": "Scooters and skateboards",
|
||||||
|
"Slušalke": "Headphones",
|
||||||
|
"Sončne celice": "Solar panels",
|
||||||
|
"Soundbar zvočniki": "Soundbars",
|
||||||
|
"Stolčki": "High chairs",
|
||||||
|
"Strižniki": "Hair clippers",
|
||||||
|
"Supi": "Paddleboards",
|
||||||
|
"Sušilni stroji": "Tumble dryers",
|
||||||
|
"Tablični računalniki": "Tablets",
|
||||||
|
"Televizorji": "Televisions",
|
||||||
|
"Tipkovnice": "Keyboards",
|
||||||
|
"Tiskalniki": "Printers",
|
||||||
|
"Tiskalniki nalepk in trakov": "Label and tape printers",
|
||||||
|
"Tobogani in gugalnice": "Slides and swings",
|
||||||
|
"Trampolini": "Trampolines",
|
||||||
|
"Uničevalci dokumentov": "Shredders",
|
||||||
|
"Visokotlačni čistilci": "Pressure washers",
|
||||||
|
"Vlažilniki in čistilci zraka": "Humidifiers and air purifiers",
|
||||||
|
"Vozički": "Strollers",
|
||||||
|
"Vrtno orodje": "Garden tools",
|
||||||
|
"Vtičnice": "Smart plugs",
|
||||||
|
"Zamrzovalniki": "Freezers",
|
||||||
|
"Zaščitna stekla": "Screen protectors",
|
||||||
|
"Zimski športi": "Winter sports",
|
||||||
|
"Zobne prhe": "Water flossers",
|
||||||
|
"Čistilci oken in tal": "Window and floor cleaners",
|
||||||
|
"Športi z žogo": "Ball sports",
|
||||||
|
"Štedilniki": "Cookers",
|
||||||
|
"Žari": "Grills",
|
||||||
|
"Žepni bralniki": "E-readers"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Phrase tables are applied in order and each entry is a global literal
|
||||||
|
// replacement, so a shorter phrase must never run before a longer one containing
|
||||||
|
// it. Groups matter as much as the order inside them: body-block sentences embed
|
||||||
|
// title-slot words ("dimenzije", "procesor", "tip izdelka"), so whole sentences are
|
||||||
|
// translated first and bare slot words last.
|
||||||
|
const SCAFFOLD_PHRASES = [
|
||||||
|
// ---- prompt scaffolding -------------------------------------------------
|
||||||
|
["Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:", "Create a new product description in {{language}} using the following variables:"],
|
||||||
|
["Star_opis_izdelka", "Old_product_description"],
|
||||||
|
["Staro_ime_izdelka", "Old_product_name"],
|
||||||
|
["OPIS IZDELKA", "PRODUCT DESCRIPTION"],
|
||||||
|
["STARO IME IZDELKA", "OLD PRODUCT NAME"],
|
||||||
|
["Uporabi spodnjo GPT predlogo.", "Use the product template below."],
|
||||||
|
["Sledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:", "Follow this product template sentence by sentence and compose a new product description:"],
|
||||||
|
["GPT predloga:", "Product template:"],
|
||||||
|
|
||||||
|
// ---- meta rule ----------------------------------------------------------
|
||||||
|
["Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki", "Start with the New product name and then continue until you have written 140 characters including spaces"],
|
||||||
|
|
||||||
|
// ---- title formula wrapper ---------------------------------------------
|
||||||
|
["Napiši novo ime izdelka po formuli:", "Write a new product name using this formula:"],
|
||||||
|
["Če je kabel za polnjenje, potem sestavi ime tako:", "If it is a charging cable, build the name like this:"],
|
||||||
|
["Ne uporabljaj vejic.", "Do not use commas."],
|
||||||
|
["Ne vključuj barve v ime.", "Do not include the colour in the name."]
|
||||||
|
];
|
||||||
|
|
||||||
|
const TITLE_SLOT_PHRASES = [
|
||||||
|
// ---- title slots (longest first) ---------------------------------------
|
||||||
|
["poln model izdelka, če lahko z besedo in ID uppercase", "full product model, with the word and the ID in uppercase when available"],
|
||||||
|
["tip izdelka (lahko tudi brez, če ni primernega izraza) sentence case", "product type in sentence case (omit when no suitable term exists)"],
|
||||||
|
["barva v eni slovenski besedi (če je možnih več barv)", "colour as a single word (when several colours are possible)"],
|
||||||
|
["barva v eni slovenski besedi (če je več barvnih variant)", "colour as a single word (when several colour variants exist)"],
|
||||||
|
["barva v eni slovenski besedi", "colour as a single word"],
|
||||||
|
["znamka in model naprave, za katere je namenjen", "brand and model of the device it is intended for"],
|
||||||
|
["model naprave za katero je namenjen", "model of the device it is intended for"],
|
||||||
|
["tip izdelka (»Giljotina« ali »Rezalnik«)", "product type (\"Guillotine\" or \"Trimmer\")"],
|
||||||
|
["znamka s pravilno kapitalizacijo", "brand with correct capitalisation"],
|
||||||
|
["diagonala v inčih in označeno z '", "screen diagonal in inches marked with '"],
|
||||||
|
["velikost zaslona v inčih - (primer 13.6'')", "screen size in inches - (example 13.6'')"],
|
||||||
|
["velikost v cm (npr. 120x200cm)", "size in cm (e.g. 120x200cm)"],
|
||||||
|
["osveževanje (če je 144Hz)", "refresh rate (when it is 144Hz)"],
|
||||||
|
["Stopnja osveževanja v Hz", "Refresh rate in Hz"],
|
||||||
|
["vrsta priključka (npr. USB C)", "connector type (e.g. USB C)"],
|
||||||
|
["vrsta vtičnice (npr. RJ45)", "socket type (e.g. RJ45)"],
|
||||||
|
["vrsta kabla (UTP/STP…)", "cable type (UTP/STP…)"],
|
||||||
|
["otroška pametna ura lowercase", "kids smartwatch in lowercase"],
|
||||||
|
["tip izdelka sentence case", "product type in sentence case"],
|
||||||
|
["vrsta izdelka lowercase", "product type in lowercase"],
|
||||||
|
["model izdelka uppercase", "product model in uppercase"],
|
||||||
|
["kapaciteta pranja [kg]", "wash capacity [kg]"],
|
||||||
|
["skupina po teži [kg]", "weight group [kg]"],
|
||||||
|
["kapaciteta v litrih", "capacity in litres"],
|
||||||
|
["tip grafične kartice", "graphics card type"],
|
||||||
|
["grafična kartica", "graphics card"],
|
||||||
|
["grelnik vode lowercase", "water heater in lowercase"],
|
||||||
|
["tip izdelka lowercase", "product type in lowercase"],
|
||||||
|
["klima z montažo", "air conditioner with installation"],
|
||||||
|
["velikost pomnilnika", "memory size"],
|
||||||
|
["moč polnjenja", "charging power"],
|
||||||
|
["polnilni kabel", "charging cable"],
|
||||||
|
["cvrtnik lowercase", "air fryer in lowercase"],
|
||||||
|
["monitor lowercase", "monitor in lowercase"],
|
||||||
|
["tip procesorja", "processor type"],
|
||||||
|
["znamka izdelka", "product brand"],
|
||||||
|
["znamka normal case", "brand in normal case"],
|
||||||
|
["model izdelka", "product model"],
|
||||||
|
["dimenzije [cm]", "dimensions [cm]"],
|
||||||
|
["širina [cm]", "width [cm]"],
|
||||||
|
["dolžina [m]", "length [m]"],
|
||||||
|
["ime igre", "game title"],
|
||||||
|
["»Uničevalec dokumentov«", "\"Shredder\""],
|
||||||
|
["»Tiskalnik nalepk«", "\"Label printer\""],
|
||||||
|
["»Plastifikator«", "\"Laminator\""],
|
||||||
|
["»postelja«", "\"bed\""],
|
||||||
|
["dimenzije", "dimensions"],
|
||||||
|
["procesor", "processor"],
|
||||||
|
["pomnilnik", "memory"],
|
||||||
|
["kapaciteta", "capacity"],
|
||||||
|
["barva", "colour"],
|
||||||
|
["kabel", "cable"],
|
||||||
|
["model", "model"]
|
||||||
|
];
|
||||||
|
|
||||||
|
const BODY_BLOCK_PHRASES = [
|
||||||
|
// ---- description body blocks (longest first) ---------------------------
|
||||||
|
["Napiši Novo ime izdelka brez procesorja, brez grafične kartice, brez pomnilnika in izpostavi en benefit", "Write the New product name without the processor, without the graphics card and without the memory, and highlight one benefit"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez procesorja, brez grafične kartice in brez pomnilnika", "Write a 100-word paragraph that DOES include the New product name without the processor, without the graphics card and without the memory"],
|
||||||
|
["Napiši material postelje (npr. »Lesena postelja« ali »Kovinska postelja«) in izpostavi en benefit ter VKLJUČI Novo ime izdelka", "Write the bed material (e.g. \"Wooden bed\" or \"Metal bed\"), highlight one benefit and DO include the New product name"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve in brez podatka o obnovljenosti", "Write a 100-word paragraph that DOES include the New product name without the colour and without the refurbished status"],
|
||||||
|
["Napiši Novo ime izdelka brez barve in brez podatka o obnovljenosti in izpostavi en benefit", "Write the New product name without the colour and without the refurbished status, and highlight one benefit"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez kapacitete pranja", "Write a 100-word paragraph that DOES include the New product name without the wash capacity"],
|
||||||
|
["Napiši Novo ime izdelka brez kapacitete pranja in izpostavi en benefit", "Write the New product name without the wash capacity, and highlight one benefit"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez skupne teže", "Write a 100-word paragraph that DOES include the New product name without the total weight"],
|
||||||
|
["Napiši Novo ime izdelka brez skupne teže in izpostavi en benefit", "Write the New product name without the total weight, and highlight one benefit"],
|
||||||
|
["Napiši Novo ime izdelka brez skupine po teži in izpostavi en benefit", "Write the New product name without the weight group, and highlight one benefit"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez dimenzije", "Write a 100-word paragraph that DOES include the New product name without the dimensions"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez dimenzij", "Write a 100-word paragraph that DOES include the New product name without the dimensions"],
|
||||||
|
["Napiši Novo ime izdelka brez dimenzije in izpostavi en benefit", "Write the New product name without the dimensions, and highlight one benefit"],
|
||||||
|
["Napiši Novo ime izdelka brez dimenzij in izpostavi en benefit", "Write the New product name without the dimensions, and highlight one benefit"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez širine", "Write a 100-word paragraph that DOES include the New product name without the width"],
|
||||||
|
["Napiši Novo ime izdelka brez širine in izpostavi en benefit", "Write the New product name without the width, and highlight one benefit"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka brez kapacitete", "Write a 100-word paragraph that DOES include the New product name without the capacity"],
|
||||||
|
["Napiši Novo ime izdelka brez kapacitete in izpostavi en benefit", "Write the New product name without the capacity, and highlight one benefit"],
|
||||||
|
["Napiši Novo ime izdelka brez diagonale in izpostavi en benefit", "Write the New product name without the screen diagonal, and highlight one benefit"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve", "Write a 100-word paragraph that DOES include the New product name without the colour"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka brez barve", "Write a 100-word paragraph that DOES include the New product name without the colour"],
|
||||||
|
["Napiši Novo ime izdelka brez barve in izpostavi en benefit", "Write the New product name without the colour, and highlight one benefit"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.", "Write a 100-word paragraph that does NOT contain the New product name."],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.", "Write a 100-word paragraph that does NOT contain the New product name."],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in NE VKLJUČI Novo ime izdelka.", "Write a 100-word paragraph that does NOT include the New product name."],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.", "Write a 100-word paragraph that DOES include the New product name."],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka.", "Write a 100-word paragraph that DOES include the New product name."],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka", "Write a 100-word paragraph that DOES include the New product name"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka", "Write a 100-word paragraph that DOES include the New product name"],
|
||||||
|
["Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi tip izdelka lowercase", "Write a 100-word paragraph that DOES include the product type in lowercase"],
|
||||||
|
["Nadaljuj iz naslova in napiši 100 besed o temu benefitu, ne vključuj Novo_ime_izdelka", "Continue from the heading and write 100 words about that benefit, without including the New product name"],
|
||||||
|
["Napiši naslov, ki izpostavlja en benefit in VKLJUČI tudi tip izdelka lowercase.", "Write a heading that highlights one benefit and DOES include the product type in lowercase."],
|
||||||
|
["Naj vključuje Novo_ime_izdelka in izpostavi en benefit", "Include the New product name and highlight one benefit"],
|
||||||
|
["Napiši Novo ime izdelka in izpostavi en benefit", "Write the New product name and highlight one benefit"],
|
||||||
|
["Napiši tip izdelka sentence case in izpostavi en benefit", "Write the product type in sentence case and highlight one benefit"],
|
||||||
|
["Izpostavi en benefit, in NE napiši Novo ime izdelka", "Highlight one benefit and do NOT write the New product name"],
|
||||||
|
["Izpostavi en benefit in NE napiši Novo ime izdelka", "Highlight one benefit and do NOT write the New product name"],
|
||||||
|
["Izpostavi en benefit in VKLJUČI Novo ime izdelka", "Highlight one benefit and DO include the New product name"],
|
||||||
|
["Zapiši Novo_ime_izdelka", "Write the New product name"],
|
||||||
|
["Napiši 3-10 tehničnih specifikacij v alinejah", "Write 3-10 technical specifications as bullet points"],
|
||||||
|
["Napiši 3–10 tehničnih specifikacij v alinejah", "Write 3-10 technical specifications as bullet points"],
|
||||||
|
["Tehnične specifikacije", "Technical specifications"],
|
||||||
|
["Novo_ime_izdelka", "New product name"],
|
||||||
|
["Novo ime izdelka", "New product name"]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Whole sentences first, bare slot words last.
|
||||||
|
const PHRASES = [...SCAFFOLD_PHRASES, ...BODY_BLOCK_PHRASES, ...TITLE_SLOT_PHRASES];
|
||||||
|
|
||||||
|
// Letters that only occur in Slovenian words — used as the residue tripwire.
|
||||||
|
const SLOVENE_LETTERS = /[čćšžđČĆŠŽĐ]/;
|
||||||
|
// Slovenian function words that survive without diacritics.
|
||||||
|
const SLOVENE_WORDS =
|
||||||
|
/\b(napi[sš]i|izpostavi|odstavek|besed|izdelka|izdelek|vklju[cč]i|brez|tudi|znamka|barva|dolg|nov[ao]?|ime|opis|predloga|uporabi|sledi|ustvari|stavek|spremenljivkami|slovensko?i?|vejic|alinejah|specifikacij|tehni[cč]n)\b/i;
|
||||||
|
|
||||||
|
function translate(text) {
|
||||||
|
let out = text;
|
||||||
|
for (const [sl, en] of PHRASES) out = out.split(sl).join(en);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function residue(text) {
|
||||||
|
// Ignore the placeholder tokens and HTML tags when scanning for leftovers.
|
||||||
|
const stripped = text.replace(/<[^>]*>/g, " ").replace(/\{\{[a-z_]+\}\}/g, " ");
|
||||||
|
const hits = [];
|
||||||
|
for (const line of stripped.split(/[\n;.]/)) {
|
||||||
|
const t = line.trim();
|
||||||
|
if (!t) continue;
|
||||||
|
if (SLOVENE_LETTERS.test(t) || SLOVENE_WORDS.test(t)) hits.push(t);
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const check = process.argv.includes("--check");
|
||||||
|
const src = JSON.parse(readFileSync(SOURCE, "utf8"));
|
||||||
|
const entries = [];
|
||||||
|
const problems = [];
|
||||||
|
|
||||||
|
for (const e of src.entries) {
|
||||||
|
const slName = String(e.name || "").trim();
|
||||||
|
const enName = CATEGORY_NAMES[slName];
|
||||||
|
if (!enName) {
|
||||||
|
problems.push(`no English name mapped for category ${JSON.stringify(slName)}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const prompt = translate(String(e.prompt || ""));
|
||||||
|
const left = residue(prompt);
|
||||||
|
if (left.length) {
|
||||||
|
problems.push(`untranslated text in ${slName} -> ${enName}:\n ${left.slice(0, 3).join("\n ")}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entries.push({
|
||||||
|
name: enName,
|
||||||
|
// Legacy names stay as match aliases so a tenant importing the original
|
||||||
|
// taxonomy still resolves to the English default formula.
|
||||||
|
aliases: [slName],
|
||||||
|
...(e.unique_id ? { unique_id: e.unique_id } : {}),
|
||||||
|
prompt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (problems.length) {
|
||||||
|
console.error(`build-default-category-prompts: ${problems.length} problem(s)`);
|
||||||
|
for (const p of problems.slice(0, 20)) console.error(` - ${p}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.sort((a, b) => a.name.localeCompare(b.name, "en"));
|
||||||
|
const out = {
|
||||||
|
version: 1,
|
||||||
|
source: "a1-category-prompts.json (translated by scripts/seed/build-default-category-prompts.mjs)",
|
||||||
|
language: "en",
|
||||||
|
note: "English platform-default category formulas. A1 Slovenija keeps a1-category-prompts.json.",
|
||||||
|
entries
|
||||||
|
};
|
||||||
|
const json = JSON.stringify(out, null, 2) + "\n";
|
||||||
|
|
||||||
|
if (check) {
|
||||||
|
let current = "";
|
||||||
|
try {
|
||||||
|
current = readFileSync(TARGET, "utf8");
|
||||||
|
} catch {
|
||||||
|
console.error("default-category-prompts.json is missing — run without --check");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (current !== json) {
|
||||||
|
console.error("default-category-prompts.json is stale — run: node scripts/seed/build-default-category-prompts.mjs");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`default-category-prompts.json is up to date (${entries.length} entries)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(TARGET, json, "utf8");
|
||||||
|
console.log(`wrote ${TARGET} (${entries.length} entries)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
Reference in New Issue
Block a user