feat(review): View prompt + Call 2 declared attrs, Create-desc gate
Shared assemble_call2 for GET .../call2/prompt and POST call2 (seed + DECLARED ATTRIBUTES + dimension fact + brand/geo; photo slots without base64). Create desc disabled until content LLM + title/short + species + wood cats/primary + edge + ≥1 figure + grade + moisture + price; show missing. figure_term_ids required for ready. Moisture defaults to Kiln dried (kd). Pin snl / liveedge in wood-cat UI. Ready-on-change via MultiSelect onCommit.
This commit is contained in:
@@ -342,3 +342,116 @@ def test_call1_sends_long_edge_dormant_while_disabled(env):
|
||||
r = client.post("/api/v1/slabs/missing/inference/call1")
|
||||
assert r.status_code == 410
|
||||
assert r.json()["error"]["code"] == "call1_disabled"
|
||||
|
||||
|
||||
def test_assemble_call2_includes_declared_attrs_and_dims(env):
|
||||
"""Call 2 text = seed + DECLARED ATTRIBUTES + dimension fact + brand/geo; shared assembler."""
|
||||
client, conn = env
|
||||
conn.execute(
|
||||
"INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES "
|
||||
"(10,'Species','species',NULL,10),"
|
||||
"(11,'Black Walnut','black-walnut',10,11),"
|
||||
"(20,'Wcat','wcat',NULL,20),"
|
||||
"(21,'Slabs & Lumber','snl',20,21),"
|
||||
"(22,'Live edge wood','liveedge',21,22)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES "
|
||||
"(6,'Edge type','pa_etype',6),(8,'Figure','pa_fig',8),"
|
||||
"(9,'Grade','pa_gr',9),(7,'Moisture','pa_mst',7)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES "
|
||||
"(321,6,'Live Edge 1 side','le1',321),"
|
||||
"(328,8,'Curl','curl',328),"
|
||||
"(342,9,'Fancy','fnc',342),"
|
||||
"(326,7,'Kiln dried','kd',326)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO woo_tags(id,name,slug,woo_id) VALUES (1,'feat-knot','feat-knot',1)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES "
|
||||
"('brand_voice','Warm and precise.',NULL),"
|
||||
"('geo_context','Pacific Northwest mill.',NULL)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO slabs(id,sku,length_in,thickness_in,width_avg_in,width_min_in,width_max_in,bdft,"
|
||||
"species_id,wood_category_ids,edge_type_term_id,figure_term_ids,grade_term_id,moisture_term_id,"
|
||||
"feat_tag_ids,title,short_description,status,server_rev,client_rev,created_at,updated_at) "
|
||||
"VALUES ('s1','SLAB-01',114.0,1.75,24.3,18.0,24.3,18.4,11,?,321,?,342,326,?,"
|
||||
"'Figured Black Walnut Live Edge Slab','short line','ready',1,1,'t','t')",
|
||||
(json.dumps([22]), json.dumps([328]), json.dumps([1])),
|
||||
)
|
||||
png = _rgba_png(200, 200)
|
||||
conn.execute(
|
||||
"INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('p9','s1',1,?, 't')",
|
||||
(png,),
|
||||
)
|
||||
conn.commit()
|
||||
from backend.inference import assemble_call2, assemble_call2_text, CALL2_SEED
|
||||
|
||||
assembled = assemble_call2(conn, "s1")
|
||||
assert assembled is not None
|
||||
text = assembled["text"]
|
||||
assert "DECLARED ATTRIBUTES" in text
|
||||
assert "species: Black Walnut" in text
|
||||
assert "primary wood category: Live edge wood" in text
|
||||
assert "edge: Live Edge 1 side" in text
|
||||
assert "figure: Curl" in text
|
||||
assert "grade: Fancy" in text
|
||||
assert "moisture: Kiln dried" in text
|
||||
assert "features: knot" in text
|
||||
assert "DIMENSION FACT BLOCK" in text
|
||||
assert "length=114.0in" in text
|
||||
assert "Brand voice: Warm and precise." in text
|
||||
assert "Geo: Pacific Northwest mill." in text
|
||||
assert CALL2_SEED.split("\n")[0] in text
|
||||
# No raw base64 in View-prompt shape
|
||||
assert "photos" in assembled
|
||||
assert assembled["photos"][0]["id"] == "p9"
|
||||
assert assembled["photos"][0]["seq"] == 1
|
||||
assert assembled["photos"][0]["bytes"] > 0
|
||||
assert "base64" not in json.dumps(assembled["photos"])
|
||||
# View prompt GET mirrors assembler (no model)
|
||||
r = client.get("/api/v1/slabs/s1/inference/call2/prompt")
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert body["text"] == text
|
||||
assert body["photos"][0]["id"] == "p9"
|
||||
assert "data:image" not in json.dumps(body)
|
||||
|
||||
|
||||
def test_call2_post_uses_same_assembler_text(env, monkeypatch):
|
||||
"""POST call2 sends the same assembled text View prompt returns."""
|
||||
client, conn = env
|
||||
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('content_llm_enabled','true',NULL)")
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES "
|
||||
"('brand_voice','voice',NULL),('geo_context','geo',NULL)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO slabs(id,sku,length_in,thickness_in,width_avg_in,width_min_in,width_max_in,bdft,"
|
||||
"title,status,server_rev,client_rev,created_at,updated_at) "
|
||||
"VALUES ('s1','SLAB-01',100,2,20,18,22,18,'Walnut Slab','ready',1,1,'t','t')"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('p1','s1',1,?, 't')",
|
||||
(_rgba_png(64, 64),),
|
||||
)
|
||||
conn.commit()
|
||||
from backend.inference import InferenceClient, assemble_call2
|
||||
|
||||
expected = assemble_call2(conn, "s1")["text"]
|
||||
captured = {}
|
||||
|
||||
def fake_chat(self, messages):
|
||||
captured["text"] = messages[0]["content"][-1]["text"]
|
||||
return "<p>ok</p>"
|
||||
|
||||
monkeypatch.setattr(InferenceClient, "chat", fake_chat)
|
||||
r = client.post("/api/v1/slabs/s1/inference/call2")
|
||||
assert r.status_code == 200, r.text
|
||||
assert captured["text"] == expected
|
||||
assert "DECLARED ATTRIBUTES" in captured["text"]
|
||||
assert "DIMENSION FACT BLOCK" in captured["text"]
|
||||
|
||||
@@ -194,3 +194,29 @@ def test_review_persists_owner_overrides(env):
|
||||
assert got["sku"] == "SLAB-99"
|
||||
# server_rev was incremented (the UPDATE fired).
|
||||
assert got["server_rev"] == 2
|
||||
|
||||
|
||||
def test_ready_requires_figure_term_ids(env):
|
||||
"""UX requires ≥1 figure — figure_term_ids is in _REQUIRED."""
|
||||
from backend.review import missing_required, compute_ready
|
||||
|
||||
slab = _slab(figure_term_ids=[])
|
||||
miss = missing_required(slab)
|
||||
assert "figure_term_ids" in miss
|
||||
ready, missing = compute_ready({**slab, "price": 1.0, "price_source": "manual",
|
||||
"title": "T", "short_description": "S",
|
||||
"edge_type_term_id": 1, "grade_term_id": 1,
|
||||
"thickness_term_id": 1, "moisture_term_id": 1})
|
||||
assert ready is False
|
||||
assert "figure_term_ids" in missing
|
||||
|
||||
ready2, missing2 = compute_ready({
|
||||
**slab,
|
||||
"figure_term_ids": [1],
|
||||
"price": 1.0, "price_source": "manual",
|
||||
"title": "T", "short_description": "S",
|
||||
"edge_type_term_id": 1, "grade_term_id": 1,
|
||||
"thickness_term_id": 1, "moisture_term_id": 1,
|
||||
})
|
||||
# May still miss other fields depending on fixture; figure must not be listed
|
||||
assert "figure_term_ids" not in missing2
|
||||
|
||||
Reference in New Issue
Block a user