This commit is contained in:
2026-08-23 20:49:40 +02:00
parent 3b678ca857
commit f3d4fb56ed
31 changed files with 3608 additions and 111 deletions
+99
View File
@@ -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.