2026-08-23 20:49:40 +02:00
|
|
|
# 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=…
|
2026-08-23 22:03:57 +02:00
|
|
|
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).
|