Files
descrybe/docs/category-formulas.md
T
2026-08-23 23:14:57 +02:00

6.0 KiB

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 createcatalog.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 fallbackprocessing.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).

Low-confidence categorisation fails the product

The categorize model always answers with some id from the list, so a reply like {"categoryId":"1","confidence":0.08} is the model saying "I do not know". Taking it at face value files a camping chair under Generators and then drives that category's formula, producing confidently wrong copy.

Below processing.DefaultMinCategorizeConfidence (0.75) the product fails with ErrCategoryNotFound and processing stops there — enhance is never called, so no credits and no provider cost are spent writing copy for a category nobody believes in. The same applies when the model returns an id outside the taxonomy.

processing: category rejected uid=1 confidence=0.08 min=0.75 reason=low_confidence
processing: item failed ... err=product category could not be determined:
            best match "1" scored 0.08, below the 0.75 minimum

The product lands in the job as failed with that message on processing_job_products.error, and raw_products.processing_status = 'failed'.

Not gated: a category that came from the feed, vector or a prior run (categorize never runs), a model that reports no confidence at all, and AI-disabled plans. Override the floor per job with StepPolicy.MinCategorizeConfidence.

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 — and Admin → AI calls shows the exact prompt and reply for the call itself (docs/ai-call-inspector.md).