This commit is contained in:
2026-08-17 21:56:09 +02:00
parent 6fcdc74843
commit a7e53ca0a3
6 changed files with 162 additions and 56 deletions
+85 -21
View File
@@ -123,6 +123,27 @@ info:
processed_product_id / raw_product_id). Use GET /products when a UUID is needed.
Display name is items[].name (title omitted when identical).
### AI descriptions (HTML body, not a short blurb)
Default processing_type full runs normalize, parse specs, fill fields, EPREL
(when enabled), categorize, then AI enhance. Enhance writes the display name
and a full product body.
Completed items[].description is that body: one HTML string (tags matching
section types h1/h2/h3/h4, p, ul) — never a JSON array and never a one- or
two-sentence slogan. Length and structure come from the assigned category
description_template (description formula): ordered sections with their own
instructions. A formula can ask for a short paragraph, 100-word blocks,
several heading/list sections, or a very large multi-section article. There
is no request-body size enum; configure length on the category. When a
category has no formula, the default is still 1-3 factual paragraphs with
limited HTML, not a headline-only stub.
items[].description on the start body is optional source text the model may
rewrite. Poll GET /products/process/{id} for the generated HTML.
meta_title / meta_description are separate short SEO fields and may be
omitted depending on company configuration.
Note: Dashboard JSON under /api/* uses session cookies + CSRF and is separate
from this public API-key surface. Other path aliases
(/categories/create, /attributes/create, /campaigns) appear next to canonical paths.
@@ -674,7 +695,10 @@ paths:
description: Enriched display title
processed_description:
type: string
description: Enriched long description text
description: |
Enriched product body as one HTML string from the category
description formula. Length follows that formula (including very
large multi-section copy), not a short summary.
status:
type: string
enum:
@@ -788,14 +812,23 @@ paths:
- Processing
summary: Start processing by EAN
description: |
Public process API (handleV1StartProcess). Upserts raw products from
items[].ean (GTIN), enqueues one processing job, returns HTTP 200 with a data envelope.
Public process API. Upserts raw products from items[].ean (GTIN), enqueues
one processing job, returns HTTP 200 with a data envelope.
Default processing_type full runs the full pipeline including AI enhance:
the model writes a retail title (name) and a full HTML product description
driven by the category description formula. That body can be long —
multiple headings, paragraphs, and lists, including very large
multi-section articles — not a one- or two-sentence summary.
Primary body: items[].ean. Alternate body on the same handler: raw_product_ids
(raw_products.id UUID list — not GET /products data[].id). Prefer items for public
integrations. On items[].ean, assertV1ProcessGates runs before EnsureRaw so plan/credit
failures cannot spam catalog writes (HTTP 402 coded envelope).
Poll GET /products/process/{id} until status is COMPLETED, then read
items[].description for the generated HTML.
Not an alias of POST /process (flat ProcessingJob / 202). Do not mix envelopes.
requestBody:
$ref: "#/components/requestBodies/StartProcessByEAN"
@@ -903,9 +936,13 @@ paths:
jobs omit items. Not the same shape as GET /process/{id} (flat ProcessingJob).
On COMPLETED items, name is the product display name (title is omitted when
identical). Internal UUIDs (id / processed_product_id / raw_product_id) are
omitted — use GET /products for those. Optional meta_title and
meta_description may be omitted depending on company configuration.
identical). description is the generated product body: one HTML string
following the category description formula (h1/h2/h3/h4, p, ul). Expect
real catalog copy — often several paragraphs and lists, sometimes a very
large article — not a one- or two-sentence blurb. Internal UUIDs (id /
processed_product_id / raw_product_id) are omitted — use GET /products for
those. Optional meta_title and meta_description are short SEO fields and
may be omitted depending on company configuration.
parameters:
- name: id
in: path
@@ -938,12 +975,13 @@ paths:
category_id: '50'
category_name: Cookers
name: VOX electric cooker EHT 6020 WG
description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven."
description: "<h1>VOX electric cooker EHT 6020 WG</h1><p>The VOX EHT 6020 WG is a free-standing electric cooker for everyday household cooking. Four ceramic hobs and a 65 L fan oven let you boil, fry, and bake at the same time, while energy class A helps keep running costs down. The 60 cm width fits a standard kitchen niche. An enamel oven cavity and removable inner door glass are designed for cleaning after heavy use.</p><p>Run sauces or pasta on the hob while the oven handles a roast or tray bake. Heat is even across the ceramic zones, and residual-heat indicators reduce the chance of accidental contact after cooking. Fan mode suits multi-level baking; conventional heat remains available for dishes that prefer still air. A full-width grill finishes cheese, gratins, and toast.</p><h2>Key features</h2><ul><li>Four electric ceramic hobs with residual-heat indicators</li><li>65 L fan oven plus grill, energy class A</li><li>60 cm free-standing body (0.6 m wide, 0.85 m high, 0.6 m deep)</li><li>Enamel cavity and removable inner door glass</li><li>60-month manufacturer warranty</li></ul><p>Install in a standard 60 cm cooker slot with the dedicated electric supply shown on the rating plate. Leave rear clearance for ventilation and use a heat-resistant worktop next to the hob. This product is for household kitchens, not commercial catering loads.</p><p>Use cookware that matches each zone so energy is not wasted around the pan. Wipe the ceramic surface regularly and empty crumbs from the oven floor to preserve appearance and efficiency. For spare parts, quote model EHT 6020 WG and the EAN on the rating plate.</p>"
attributes:
brand: Vox
product_model: EHT6020WG
main_image: https://images.example.com/products/vox-eht6020wg.jpg
more_images: null
more_images:
- https://images.example.com/products/vox-eht6020wg-side.jpg
eprel:
id: "1234567"
label: https://eprel.ec.europa.eu/label/Example
@@ -4389,7 +4427,9 @@ paths:
summary: Processing job status (raw_product_ids surface)
description: |
Public processing job JSON: status, counts, timestamps, processing_type.
Omits current_step, step_progress, and company_id. Completed jobs still include items[].
Omits current_step, step_progress, and company_id. Completed jobs still include
items[] with the same AI HTML description contract as GET /products/process/{id}
(category formula body, not a one- or two-sentence blurb).
parameters:
- $ref: "#/components/parameters/ID"
responses:
@@ -4421,6 +4461,7 @@ paths:
status: processed
name: Acme Wireless Earbuds ANC Black
category: Headphones
description: "<h1>Acme Wireless Earbuds ANC Black</h1><p>Acme Wireless Earbuds ANC Black are in-ear headphones for commuting and desk work. Active noise cancellation, a secure fit, and a charging case cover a full workday of listening. The compact buds sit flush enough for glasses and hats, and the case fits a jacket pocket.</p><p>Pair over Bluetooth and switch between a phone and a laptop with the onboard controls. Transparency mode lets nearby voices through without removing the buds. The default ear-tip set covers common sizes; swap tips if bass feels weak or the seal slips during a walk.</p><h2>In the box</h2><ul><li>Wireless earbuds (left and right)</li><li>Charging case with USB-C cable</li><li>Three ear-tip sizes</li><li>Quick-start guide</li></ul><p>Charge the case before first use. Keep the charging contacts dry and store the buds in the case so they sleep and recharge between sessions. For replacement tips or a spare case, quote the product name and the EAN on the box.</p>"
total_items: 1
'400':
description: Path id is not a UUID.
@@ -5133,7 +5174,10 @@ components:
description: Optional product title seed for enrichment
description:
type: string
description: Optional long description seed for enrichment
description: |
Optional source/seed text. AI enhance rewrites this into the
category description-formula HTML body. It is not the final
poll payload and does not cap length to a short summary.
specifications:
type: array
description: Optional key/value specification pairs for attribute hints
@@ -5193,10 +5237,13 @@ components:
Not PresentProduct.id. Use GET /products data[].raw_product_id.
processing_type:
description: |
full (default); named steps category|title|description|attributes (string or
full (default) runs the whole pipeline including AI enhance, which
writes formula-driven HTML descriptions (not a one- or two-sentence
blurb). Named steps category|title|description|attributes (string or
array of those steps); dual-mode also accepts v2 dashboard types such as
normalize_only, enhance_only, attributes_only, eprel_only, categorize_only
(ParseV1ProcessingType). Alias processingType accepted when values match.
The description step still emits the category formula HTML body.
oneOf:
- type: string
enum:
@@ -5550,8 +5597,13 @@ components:
type: string
description:
type: string
description: |
Product body. After processing this is the AI HTML description
(category formula), not a one- or two-sentence summary.
processed_description:
type: string
description: |
Enriched HTML body from AI enhance. Same contract as ProcessItem.description.
status:
type: string
example: completed
@@ -5664,8 +5716,16 @@ components:
nullable: true
title_template:
nullable: true
description: |
Title formula for AI enhance. Controls how processed name is built
(type, brand, model, and other slots).
description_template:
nullable: true
description: |
Description formula for AI enhance. Ordered sections (h1/h2/h3/h4, p, ul)
with per-section instructions. Controls HTML structure and length of
processed items[].description — short, long, or very large copy.
Not a one- or two-sentence cap.
created_at:
type: string
format: date-time
@@ -6297,10 +6357,11 @@ components:
description: |
One COMPLETED process line (public contract). Successful items
expose category as the human-readable display name (category_id holds
categories.unique_id), a description string that may include section
HTML (h1/h2/h3/h4, p, ul — never a JSON array), optional SEO meta_title /
meta_description (plain text; some companies omit these fields),
optional eprel object or null, clean attributes, and images.
categories.unique_id), a description string that is generated HTML
product copy (h1/h2/h3/h4, p, ul — never a JSON array and never a one-
or two-sentence slogan), optional SEO meta_title / meta_description
(plain text; some companies omit these fields), optional eprel object
or null, clean attributes, and images.
Product display name is name (primary). title is omitted when identical to name.
Internal UUIDs (id, processed_product_id, raw_product_id) are omitted from this
public shape — use catalog APIs when a product UUID is required.
@@ -6317,8 +6378,6 @@ components:
description: |
Human-readable category display name (same value as category_name when
resolved). Prefer this for UI. Machine unique_id is category_id.
ASSUMPTION: historically this field held categories.unique_id; clients
that need the opaque id must read category_id (additive).
category_id:
type: string
nullable: true
@@ -6356,9 +6415,14 @@ components:
type: string
nullable: true
description: |
Product body description as a string — never a one-element JSON array.
May include section HTML tags (h1/h2/h3/h4, p, ul). Plain-text
descriptions remain plain. Optional meta_description stays plain SEO text.
Generated product body as one HTML string — never a JSON array and
never a one- or two-sentence slogan. AI enhance follows the assigned
category description_template (description formula): ordered sections
of type h1, h2, h3, h4, p, and ul, each with its own instructions.
Output can be a short paragraph, several 100-word blocks, or a very
large multi-section article. When no formula is set, expect 1-3
factual paragraphs with limited HTML. Optional meta_description stays
plain SEO text and is not a substitute for this field.
attributes:
type: object
additionalProperties: true
@@ -6477,7 +6541,7 @@ components:
category_id: '50'
category_name: Cookers
name: VOX electric cooker EHT 6020 WG
description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven. Energy class A with practical everyday capacity."
description: "<h1>VOX electric cooker EHT 6020 WG</h1><p>The VOX EHT 6020 WG is a free-standing electric cooker for everyday household cooking. Four ceramic hobs and a 65 L fan oven let you boil, fry, and bake at the same time, while energy class A helps keep running costs down. The 60 cm width fits a standard kitchen niche. An enamel oven cavity and removable inner door glass are designed for cleaning after heavy use.</p><p>Run sauces or pasta on the hob while the oven handles a roast or tray bake. Heat is even across the ceramic zones, and residual-heat indicators reduce the chance of accidental contact after cooking. Fan mode suits multi-level baking; conventional heat remains available for dishes that prefer still air. A full-width grill finishes cheese, gratins, and toast.</p><h2>Key features</h2><ul><li>Four electric ceramic hobs with residual-heat indicators</li><li>65 L fan oven plus grill, energy class A</li><li>60 cm free-standing body (0.6 m wide, 0.85 m high, 0.6 m deep)</li><li>Enamel cavity and removable inner door glass</li><li>60-month manufacturer warranty</li></ul><p>Install in a standard 60 cm cooker slot with the dedicated electric supply shown on the rating plate. Leave rear clearance for ventilation and use a heat-resistant worktop next to the hob. This product is for household kitchens, not commercial catering loads.</p><p>Use cookware that matches each zone so energy is not wasted around the pan. Wipe the ceramic surface regularly and empty crumbs from the oven floor to preserve appearance and efficiency. For spare parts, quote model EHT 6020 WG and the EAN on the rating plate.</p>"
attributes:
brand: Vox
product_model: EHT6020WG
@@ -467,6 +467,44 @@ func TestV1OpenAPIYAMLOmitsInternalPromptWording(t *testing.T) {
}
}
func TestV1OpenAPIYAMLDocumentsAIDescriptionHTML(t *testing.T) {
t.Parallel()
doc := string(v1OpenAPIYAML)
for _, needle := range []string{
"AI descriptions (HTML body, not a short blurb)",
"description_template (description formula)",
"one- or two-sentence",
"<h1>VOX electric cooker EHT 6020 WG</h1>",
"<h2>Key features</h2>",
} {
if !strings.Contains(doc, needle) {
t.Fatalf("openapi missing AI description contract text %q", needle)
}
}
root := mustParseOpenAPIRoot(t, v1OpenAPIYAML)
comps, _ := root["components"].(map[string]any)
examples, _ := comps["examples"].(map[string]any)
completed, _ := examples["ProcessCompletedExample"].(map[string]any)
val, _ := completed["value"].(map[string]any)
data, _ := val["data"].(map[string]any)
items, _ := data["items"].([]any)
if len(items) == 0 {
t.Fatal("ProcessCompletedExample: expected items")
}
item, _ := items[0].(map[string]any)
desc, _ := item["description"].(string)
for _, tag := range []string{"<h1>", "<p>", "<h2>", "<ul>", "<li>"} {
if !strings.Contains(desc, tag) {
t.Fatalf("ProcessCompletedExample description must include %s (got %q)", tag, desc)
}
}
if len(desc) < 800 {
t.Fatalf("ProcessCompletedExample description too short (%d chars); expected full AI HTML body", len(desc))
}
}
func TestV1OpenAPIYAMLProcessItemCompletedExamplesOmitLeakyShape(t *testing.T) {
t.Parallel()
root := mustParseOpenAPIRoot(t, v1OpenAPIYAML)