Lab 03 — Product-Listing Generator Service — AI Infrastructure for Real Business Problems
Last updated 10/17/2026
Course › Hands-on track › Lab 03

Lab 03 — Product-Listing Generator Service

商品文案生成服務 · Hands-on track: AI Infrastructure for Real Business Problems

Code: labs/03/ · Uses: data/products.csv, data/docs/brand_voice.md

Business Problem — 商業問題

Kettle & Kiln adds 10–20 SKUs a season. Every new product needs, in English and Traditional Chinese (Taiwan):

The owner writes these at night. A freelancer once wrote "microwave safe" on a gold-rimmed mug, and that led to returns and a chargeback.

We want a service, not a chat window: it takes a SKU, returns validated copy, batch-generates a whole collection, and exports a Shopify-importable CSV with everything unpublished until a human approves it. It must never invent specs (a capacity, a dimension, "lead-free") and must never make the wrong care claim.

Architecture — 系統架構

flowchart LR
    U[Staff / script] -->|POST /v1/listings sku| API[FastAPI listing-api :8003]
    U -->|POST /v1/batches| API
    API --> CAT[catalog.py<br/>data/products.csv]
    API --> GEN[generator.py]
    GEN -->|chat_json + validator| LLM[llmkit → Ollama / cloud]
    GEN --> RULES[rules.py<br/>hard + soft business rules]
    RULES -->|problems → repair turn| LLM
    API --> JOBS[jobs.py thread pool]
    JOBS --> CSV[out/*.csv Shopify format<br/>Published=FALSE]

Validation runs in two layers:

  1. Schema (Pydantic Listing): lengths and counts, e.g. title 10–70 chars, 3–5 bullets.
  2. Business rules (rules.py):
    • Hard rules, never publishable:
      • banned hype words
      • claims not in the spec ("lead-free", "certified", …)
      • any number + unit that isn't in the spec
      • "microwave safe" when the care text says otherwise
      • Simplified characters in zh-TW copy
    • Soft rules, style only:
      • title includes the product name
      • description 60–120 words
      • no ALL-CAPS
      • bullets of 15 words or fewer

Rule problems are sent back to the model as a repair turn (LISTING_MAX_REPAIRS, default 2). If only soft rules still fail after the repairs, the draft is returned with needs_review: true. If a hard rule still fails, the API returns 422, and nothing unsafe gets through.

Prerequisites — 先決條件

Step-by-Step Setup — 逐步設定

cd hands-on/labs/03
uv venv --python 3.12 && source .venv/bin/activate
uv pip install -e ../common -r requirements.txt
cp .env.example .env
uvicorn app.main:app --port 8003            # http://localhost:8003/docs

Single listing:

curl -s localhost:8003/v1/listings -H 'content-type: application/json' \
  -d '{"sku":"KK-MUG-001","lang":"en"}' | python -m json.tool
curl -s localhost:8003/v1/listings -H 'content-type: application/json' \
  -d '{"sku":"KK-MUG-003","lang":"zh-TW"}' | python -m json.tool   # the gold-rim mug

Batch + Shopify CSV:

curl -s localhost:8003/v1/batches -H 'content-type: application/json' \
  -d '{"skus":["KK-MUG-001","KK-TEA-001"],"lang":"en"}'          # → {"job_id": "…", "status": "queued", "total": 2, ...}
curl -s localhost:8003/v1/batches/<job_id>
curl -s -o listings.csv localhost:8003/v1/batches/<job_id>/shopify.csv
# or without the server (cron-friendly):
python batch_cli.py --skus KK-MUG-001 KK-TEA-001 --out out/listings.csv

The CSV has a leading Needs Review column. Filter on it, then delete the column before importing in Shopify. Published is always FALSE.

Container:

docker compose up --build        # Ollama on the host is reached via host.docker.internal

Code Walkthrough — 程式碼導覽

How to Test — 測試方法

LLM_PROVIDER=mock pytest -q      # 8 tests

The tests cover:

What was run on the build box — 實測紀錄

Cost & Performance Notes — 成本與效能

Common Failures & Fixes — 常見錯誤與修正

Symptom Cause Fix
422 with "quantity '350 ml' is not in the spec" model invented or reformatted a number good: the rule worked. Add the real value to products.csv if it's true
Every listing needs_review model too small, or word-count rules too strict for the language larger model; for zh-TW word count is skipped (no spaces)
False positive on "Set of 4" number rule too broad rule compares numbers only, so make sure the spec mentions the count
Container: Connection refused LLM_BASE_URL points to localhost inside container compose sets host.docker.internal; on Linux keep extra_hosts: host-gateway
Cloud provider in Docker ignores your key/URL compose's LLM_BASE_URL default overrides .env set LLM_BASE_URL explicitly in .env when using a cloud provider
Shopify import rejects file the extra Needs Review column / missing columns for your store delete the column; compare headers with a fresh Shopify export

Stretch Exercise — 延伸練習

  1. Add a hard rule: if spec.final_sale is true, the description must contain "final sale" (en) or "恕不退換" (zh-TW). Write the test first.
  2. Add POST /v1/listings/{sku}/approve that records the approver and flips the row to Published=TRUE in the next export.

Deliverable Checklist — 交付清單

↑