feat(call2): short/long desc structure in seed (not brand voice)

Call 2 seed now requires benefit-first short + four-part long HTML, returned
as JSON. POST parses and persists both fields; Create-desc gate no longer
requires short (Call 2 writes it). Brand voice/geo stay Settings-injected.
This commit is contained in:
grokbot_ty
2026-09-26 10:35:53 -07:00
parent 076eb17db6
commit c25e3f4927
8 changed files with 242 additions and 41 deletions
+90 -13
View File
@@ -106,10 +106,34 @@ CALL1_SEED = (
'"feature_tag_ids": [<id>,...], "feature_tag_confidence": <0-1>}'
)
CALL2_SEED = (
"Write the long product description for the slab from the supplied photos and listing "
"facts. The name and short description are already built, so do not repeat them. Use the "
"brand voice and geo context given. The dimensions in the fact block are exact: use those "
"strings and invent no numbers. Write prose a premium lumber buyer would read."
"Write WooCommerce listing copy for this slab from the photos and listing facts below.\n"
"You write BOTH the short description and the long description.\n"
"\n"
"Rules that always apply:\n"
"- Dimensions in the DIMENSION FACT BLOCK are exact: use those strings; invent no numbers.\n"
"- Use taxonomy names from DECLARED ATTRIBUTES; do not invent species, edge, figure, grade, "
"moisture, or features.\n"
"- Use the Brand voice and Geography sections when provided. If Brand voice is empty, stay "
"neutral. Never invent return, warranty, or shipping policies.\n"
"- The product name may already appear in DECLARED ATTRIBUTES — do not invent a new title.\n"
"\n"
"SHORT DESCRIPTION (~50 words — most important on-page copy; plain text, no HTML):\n"
"1. Lead with the outcome or problem solved (benefit), not material-first.\n"
"2. Add the key differentiator or most important spec.\n"
"3. Close with a trust signal or reason to act.\n"
"Do NOT lead with a species + material list.\n"
"\n"
"LONG DESCRIPTION (HTML for the WooCommerce visual editor — short paragraphs, "
"<ul>/<li> for bullets, <strong> for benefit leads; no markdown). Four parts, in order:\n"
"1. Hook — one sentence restating the core benefit.\n"
"2. Benefits — 3–5 bullets; each benefit-first, then the supporting spec.\n"
"3. Full specs — clean list from DECLARED ATTRIBUTES + DIMENSION FACT BLOCK "
"(dims, species, moisture, edge, figure, grade, etc.).\n"
"4. Trust close — return/warranty/brand; use Brand voice when present; otherwise a short "
"neutral close with no invented policies.\n"
"\n"
"Return ONE JSON object only (no prose outside JSON) with exactly these keys:\n"
'{"short_description": "<plain text ~50 words>", "description": "<HTML long description>"}'
)
_SEEDS = {"call1": CALL1_SEED, "call2": CALL2_SEED}
_THRESHOLD = 0.7
@@ -532,11 +556,9 @@ def build_declared_attributes(conn, slab: dict) -> str:
f"features: {', '.join(_feat_names(conn, feat_ids)) or '(none)'}",
]
title = (slab.get("title") or "").strip()
short = (slab.get("short_description") or "").strip()
if title:
lines.append(f"title: {title}")
if short:
lines.append(f"short_description: {short}")
# short_description is Call 2 output — do not feed a prior value back into the prompt.
return "\n".join(lines)
@@ -659,6 +681,52 @@ async def call2_prompt(slab_id: str) -> dict:
}
def parse_call2_response(raw: str) -> tuple[str | None, str]:
"""Split Call 2 model output into (short_description, description).
Preferred shape is a JSON object with short_description + description.
If JSON is missing or incomplete, treat the whole body as description HTML
and leave short_description None (caller keeps any prior short).
"""
text = (raw or "").strip()
if not text:
return None, ""
# Strip common markdown fences around JSON.
body = text
if body.startswith("```"):
lines = body.split("\n")
if lines and lines[0].startswith("```"):
lines = lines[1:]
if lines and lines[-1].strip() == "```":
lines = lines[:-1]
body = "\n".join(lines).strip()
try:
obj = json.loads(body)
except (ValueError, TypeError):
# Try to find a JSON object embedded in prose.
start = body.find("{")
end = body.rfind("}")
if start >= 0 and end > start:
try:
obj = json.loads(body[start : end + 1])
except (ValueError, TypeError):
return None, text
else:
return None, text
if not isinstance(obj, dict):
return None, text
short = obj.get("short_description")
desc = obj.get("description")
if isinstance(short, str):
short = short.strip() or None
else:
short = None
if isinstance(desc, str) and desc.strip():
return short, desc.strip()
# JSON without a usable description — fall back to raw.
return short, text
@router.post("/slabs/{slab_id}/inference/call2")
async def call2(slab_id: str) -> dict:
conn = get_connection()
@@ -686,13 +754,22 @@ async def call2(slab_id: str) -> dict:
_log_call(
conn, slab_id, 2, assembled["prompt_revision_id"], model, messages, raw, latency, photo_meta,
)
# Store as description, mark content_source=llm.
conn.execute(
"UPDATE slabs SET description=?, content_source='llm', updated_at=? WHERE id=?",
(raw.strip(), _now(), slab_id),
)
short, description = parse_call2_response(raw)
if short:
conn.execute(
"UPDATE slabs SET short_description=?, description=?, content_source='llm', updated_at=? WHERE id=?",
(short, description, _now(), slab_id),
)
else:
conn.execute(
"UPDATE slabs SET description=?, content_source='llm', updated_at=? WHERE id=?",
(description, _now(), slab_id),
)
conn.commit()
return {"ok": True, "description": raw.strip()}
out: dict[str, Any] = {"ok": True, "description": description}
if short:
out["short_description"] = short
return out
def server_description(slab: dict) -> str:
+87 -3
View File
@@ -77,6 +77,24 @@ def test_call1_seed_is_locked_expert_prompt(env):
assert "character\":" not in CALL1_SEED.replace(" ", "")
def test_call2_seed_has_short_long_structure(env):
"""Call 2 seed carries short+long structure and JSON dual output (not brand voice)."""
client, _conn = env
from backend.inference import CALL2_SEED
body = client.get("/api/v1/prompts/call2").json()
assert body["active_content"] == CALL2_SEED
assert "SHORT DESCRIPTION" in CALL2_SEED
assert "LONG DESCRIPTION" in CALL2_SEED
assert "Lead with the outcome" in CALL2_SEED
assert "Hook" in CALL2_SEED
assert "Brand voice and Geography sections when provided" in CALL2_SEED
assert "Whidbey" not in CALL2_SEED
assert "Pacific Northwest" not in CALL2_SEED
assert '"short_description"' in CALL2_SEED
assert '"description"' in CALL2_SEED
def test_taxonomy_lists_call1_closed_shape():
"""#86: Call 1 lists are closed Woo picks only — no grade/thickness/moisture; feat names stripped."""
import tempfile, os, shutil
@@ -177,10 +195,47 @@ def test_call1_hard_disabled_410(env, monkeypatch):
assert conn.execute("SELECT COUNT(*) AS n FROM inference_log").fetchone()["n"] == 0
def test_call2_stores_description(env, monkeypatch):
def test_call2_stores_short_and_description(env, monkeypatch):
"""Call 2 persists both short_description and description from JSON output."""
client, conn = env
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('content_llm_enabled','true',NULL)")
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,width_avg_in,bdft,title,status,server_rev,client_rev,created_at,updated_at) VALUES ('s1','SLAB-01',100,2,20,18,'Walnut Slab','ready',1,1,'t','t')")
conn.execute(
"INSERT INTO slabs(id,sku,length_in,thickness_in,width_avg_in,bdft,title,short_description,"
"status,server_rev,client_rev,created_at,updated_at) "
"VALUES ('s1','SLAB-01',100,2,20,18,'Walnut Slab','old short','ready',1,1,'t','t')"
)
conn.execute("INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('p1','s1',1,X'89504E47','t')")
conn.commit()
from backend.inference import InferenceClient
import json as _json
payload = _json.dumps({
"short_description": "Turn a blank into a showpiece table — kiln-dried walnut with live edge character. Ready to finish.",
"description": "<p><strong>Showpiece ready.</strong></p><ul><li><strong>Stable:</strong> kiln dried</li></ul>",
})
monkeypatch.setattr(InferenceClient, "chat", lambda self, messages: payload)
r = client.post("/api/v1/slabs/s1/inference/call2")
assert r.status_code == 200, r.text
body = r.json()
assert "Showpiece ready" in body["description"] or "kiln" in body["description"]
assert body["short_description"].startswith("Turn a blank")
row = conn.execute(
"SELECT short_description, description, content_source FROM slabs WHERE id='s1'"
).fetchone()
assert row["short_description"].startswith("Turn a blank")
assert "<p>" in row["description"]
assert row["content_source"] == "llm"
def test_call2_html_fallback_keeps_prior_short(env, monkeypatch):
"""Non-JSON Call 2 body becomes description only; prior short is left alone."""
client, conn = env
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('content_llm_enabled','true',NULL)")
conn.execute(
"INSERT INTO slabs(id,sku,length_in,thickness_in,width_avg_in,bdft,title,short_description,"
"status,server_rev,client_rev,created_at,updated_at) "
"VALUES ('s1','SLAB-01',100,2,20,18,'Walnut Slab','keep me','ready',1,1,'t','t')"
)
conn.execute("INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('p1','s1',1,X'89504E47','t')")
conn.commit()
from backend.inference import InferenceClient
@@ -189,11 +244,33 @@ def test_call2_stores_description(env, monkeypatch):
r = client.post("/api/v1/slabs/s1/inference/call2")
assert r.status_code == 200, r.text
assert r.json()["description"] == "<p>A fine walnut slab.</p>"
row = conn.execute("SELECT description, content_source FROM slabs WHERE id='s1'").fetchone()
assert "short_description" not in r.json()
row = conn.execute(
"SELECT short_description, description, content_source FROM slabs WHERE id='s1'"
).fetchone()
assert row["short_description"] == "keep me"
assert row["description"] == "<p>A fine walnut slab.</p>"
assert row["content_source"] == "llm"
def test_parse_call2_response_json_and_fenced():
from backend.inference import parse_call2_response
import json as _json
short, desc = parse_call2_response(
_json.dumps({"short_description": " Benefit first. ", "description": " <p>Long</p> "})
)
assert short == "Benefit first."
assert desc == "<p>Long</p>"
fenced = "```json\n" + _json.dumps({"short_description": "S", "description": "<p>D</p>"}) + "\n```"
short2, desc2 = parse_call2_response(fenced)
assert short2 == "S"
assert desc2 == "<p>D</p>"
short3, desc3 = parse_call2_response("<p>only long</p>")
assert short3 is None
assert desc3 == "<p>only long</p>"
def test_call1_disabled_even_when_inference_off(env):
"""Call 1 hard-disable wins over inference_enabled=false (still 410, not 409)."""
client, conn = env
@@ -407,6 +484,13 @@ def test_assemble_call2_includes_declared_attrs_and_dims(env):
assert "Brand voice: Warm and precise." in text
assert "Geo: Pacific Northwest mill." in text
assert CALL2_SEED.split("\n")[0] in text
assert "SHORT DESCRIPTION" in text
assert "LONG DESCRIPTION" in text
assert "short_description" in CALL2_SEED
assert '"description"' in CALL2_SEED or "\"description\"" in CALL2_SEED
# Prior short must not be fed back into the prompt (Call 2 writes it).
assert "short_description: short line" not in text
assert "Write BOTH" in CALL2_SEED or "BOTH the short" in CALL2_SEED
# No raw base64 in View-prompt shape
assert "photos" in assembled
assert assembled["photos"][0]["id"] == "p9"