fix(inference): #86 tighten Call 1 prompt and closed lists
Ship locked expert Call 1 seed: order cats before edge, species=best confidence, mill-count edge rules, Wild for cookies/burl, multi figure, feature_tag_ids only (no free-text bags, no grade). LISTS keys match the seed; feat- prefix stripped for the model. Gate and review UI apply feature_tag_ids. Seed auto-activates on deploy when still seed-authored.
This commit is contained in:
+124
-46
@@ -61,19 +61,47 @@ def _image_part_from_processed(png_bytes: bytes) -> dict:
|
||||
b64 = base64.b64encode(fitted).decode()
|
||||
return {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
|
||||
|
||||
# Locked Call 1 seed (#86 / METADATA-40): expert helper + closed Woo lists only.
|
||||
CALL1_SEED = (
|
||||
"Classify the slab's species, wood categories, edge type, and figure from the supplied "
|
||||
"photos, choosing ONLY from the supplied synced taxonomy lists (invent no category, term, "
|
||||
"or id outside them). Map free-text observations (character, inclusions, voids, checks) to "
|
||||
"the supplied feature-tag list only. Grade is NOT suggested — leave it out.\n"
|
||||
"Return a single JSON object with EXACTLY these top-level keys, in this shape:\n"
|
||||
"{\"species_id\": <id or null>, \"species_confidence\": <0-1>, "
|
||||
"\"wood_category_ids\": [<id>, ...], \"wood_category_confidence\": <0-1>, "
|
||||
"\"edge_type_term_id\": <id or null>, \"edge_confidence\": <0-1>, "
|
||||
"\"figure_term_ids\": [<id>, ...], \"figure_confidence\": <0-1>, "
|
||||
"\"character\": [...], \"inclusions\": [...], \"voids\": [...], \"checks\": [...]}\n"
|
||||
"Each *_confidence is a top-level sibling of its field (do NOT nest confidences under a "
|
||||
"'confidence' object). Use 0-1 numbers; set a field to null / [] when you cannot identify it."
|
||||
"You are a wood identification expert for retail lumber slabs. Judge only from the photos "
|
||||
"and the LISTS below. Use list ids only — never invent a name or id.\n"
|
||||
"\n"
|
||||
"Work in this order: (1) species, (2) wood categories, (3) edge type, (4) figure, "
|
||||
"(5) feature tags. Decide wood categories before edge type — edge rules depend on product form.\n"
|
||||
"\n"
|
||||
"For each field, pick from its list and give a confidence 0–1 (1 = certain, 0 = no idea). "
|
||||
"Prefer null or [] over a weak guess.\n"
|
||||
"\n"
|
||||
"Species — which tree species is this slab. Match grain, color, and figure to the species "
|
||||
"list. If more than one entry is plausible, return only the single highest-confidence match "
|
||||
"(one id). Null only when none are credible.\n"
|
||||
"\n"
|
||||
"Wood categories — product form / how it will be sold (live edge, dimensional, cookies & "
|
||||
"burl slices, turning blanks, etc.). Choose every wood_categories id that clearly fits; "
|
||||
"often one, sometimes more. [] if none fit. Finish this field before edge type.\n"
|
||||
"\n"
|
||||
"Edge type — the long-edge mill profile. One id from edge_terms, or null only if none fit.\n"
|
||||
" Meanings (match the closest list name):\n"
|
||||
" • Square / square edge — both long edges milled straight (2 milled long edges).\n"
|
||||
" • Live one side / Live Edge 1 side — one long edge milled, the other natural bark or "
|
||||
"waney (1 milled long edge).\n"
|
||||
" • Live both / Live edge 2 sides — neither long edge milled; both natural "
|
||||
"(0 milled long edges).\n"
|
||||
" • Wild edge — use when the three mill-count cases do not fit, including cookies and burl "
|
||||
"cuts/slices (no conventional slab long-edge pair). Prefer Wild for those forms rather than null.\n"
|
||||
"\n"
|
||||
"Figure — visible figure in the wood (curl, quilt, spalt, plain, burl, …). Zero or more ids "
|
||||
"from figure_terms that clearly apply.\n"
|
||||
"\n"
|
||||
"Feature tags — defects and character marks the store tags for shoppers (knots, checks, "
|
||||
"inclusions, …). Zero or more ids from feature_tags. If that list is empty, return [].\n"
|
||||
"\n"
|
||||
"Return one JSON object with exactly these keys:\n"
|
||||
'{"species_id": <id|null>, "species_confidence": <0-1>, '
|
||||
'"wood_category_ids": [<id>,...], "wood_category_confidence": <0-1>, '
|
||||
'"edge_type_term_id": <id|null>, "edge_confidence": <0-1>, '
|
||||
'"figure_term_ids": [<id>,...], "figure_confidence": <0-1>, '
|
||||
'"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 "
|
||||
@@ -100,19 +128,53 @@ def _hash(text: str) -> str:
|
||||
|
||||
|
||||
def _seed_prompts(conn) -> None:
|
||||
"""Ensure each call has a prompt row and the shipped seed revision.
|
||||
|
||||
If the shipped seed hash is new and the active revision is still seed-authored
|
||||
(or missing), activate the new seed so deploys pick up CALL1_SEED / CALL2_SEED
|
||||
updates without clobbering an owner-activated API revision.
|
||||
"""
|
||||
for name, seed in _SEEDS.items():
|
||||
if conn.execute("SELECT 1 FROM prompts WHERE name=?", (name,)).fetchone():
|
||||
continue
|
||||
cur = conn.execute(
|
||||
"INSERT INTO prompts(name, active_revision_id, created_at, updated_at) VALUES (?,?,?,?)",
|
||||
(name, None, _now(), _now()),
|
||||
)
|
||||
pid = cur.lastrowid
|
||||
cur2 = conn.execute(
|
||||
"INSERT INTO prompt_revisions(prompt_id, content, content_hash, created_at, created_by) VALUES (?,?,?,?,?)",
|
||||
(pid, seed, _hash(seed), _now(), "seed"),
|
||||
)
|
||||
conn.execute("UPDATE prompts SET active_revision_id=? WHERE id=?", (cur2.lastrowid, pid))
|
||||
prow = conn.execute("SELECT id, active_revision_id FROM prompts WHERE name=?", (name,)).fetchone()
|
||||
if prow is None:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO prompts(name, active_revision_id, created_at, updated_at) VALUES (?,?,?,?)",
|
||||
(name, None, _now(), _now()),
|
||||
)
|
||||
pid = cur.lastrowid
|
||||
active_id = None
|
||||
else:
|
||||
pid = prow[0]
|
||||
active_id = prow[1]
|
||||
h = _hash(seed)
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM prompt_revisions WHERE prompt_id=? AND content_hash=?",
|
||||
(pid, h),
|
||||
).fetchone()
|
||||
if existing is None:
|
||||
cur2 = conn.execute(
|
||||
"INSERT INTO prompt_revisions(prompt_id, content, content_hash, created_at, created_by) "
|
||||
"VALUES (?,?,?,?,?)",
|
||||
(pid, seed, h, _now(), "seed"),
|
||||
)
|
||||
seed_rev_id = cur2.lastrowid
|
||||
else:
|
||||
seed_rev_id = existing[0]
|
||||
# Activate shipped seed when nothing active, or active is still a seed revision.
|
||||
activate = False
|
||||
if active_id is None:
|
||||
activate = True
|
||||
else:
|
||||
act = conn.execute(
|
||||
"SELECT created_by FROM prompt_revisions WHERE id=?", (active_id,)
|
||||
).fetchone()
|
||||
if act is None or act[0] == "seed":
|
||||
activate = True
|
||||
if activate and active_id != seed_rev_id:
|
||||
conn.execute(
|
||||
"UPDATE prompts SET active_revision_id=?, updated_at=? WHERE id=?",
|
||||
(seed_rev_id, _now(), pid),
|
||||
)
|
||||
|
||||
|
||||
def _active_prompt(conn, call: str) -> tuple[int, str] | None:
|
||||
@@ -243,6 +305,8 @@ async def call1(slab_id: str) -> dict:
|
||||
if not base or not key:
|
||||
return JSONResponse(status_code=409, content=_error("inference_unconfigured", "Inference is not configured"))
|
||||
client = InferenceClient(base, key, model)
|
||||
_seed_prompts(conn)
|
||||
conn.commit()
|
||||
act = _active_prompt(conn, "call1")
|
||||
prompt_text = act[1] if act else CALL1_SEED
|
||||
# Build vision message with processed photos (METADATA-35: long edge <= 1024) + taxonomy.
|
||||
@@ -252,7 +316,7 @@ async def call1(slab_id: str) -> dict:
|
||||
).fetchall()
|
||||
image_parts = [_image_part_from_processed(p[0]) for p in photos]
|
||||
tax = _taxonomy_lists(conn)
|
||||
text = (f"{prompt_text}\n\nSYNCED TAXONOMY (only legal choices):\n{json.dumps(tax)}\n\n"
|
||||
text = (f"{prompt_text}\n\nLISTS:\n{json.dumps(tax, ensure_ascii=False)}\n\n"
|
||||
f"SKU={row['sku']} length_in={row['length_in']} thickness_in={row['thickness_in']}")
|
||||
messages = [{"role": "user", "content": image_parts + [{"type": "text", "text": text}]}]
|
||||
t0 = time.time()
|
||||
@@ -270,19 +334,37 @@ async def call1(slab_id: str) -> dict:
|
||||
return {"ok": True, "suggestions": suggestions}
|
||||
|
||||
|
||||
def _feat_display_name(name: str) -> str:
|
||||
"""Strip leading feat- / feat_ for the model (#86); Woo id stays authoritative."""
|
||||
n = (name or "").strip()
|
||||
low = n.lower()
|
||||
for prefix in ("feat-", "feat_"):
|
||||
if low.startswith(prefix):
|
||||
return n[len(prefix):].strip() or n
|
||||
return n
|
||||
|
||||
|
||||
def _taxonomy_lists(conn) -> dict:
|
||||
# METADATA-10: categories are leaves only; parents group only. Resolve groupers BY SLUG
|
||||
# (never hardcoded id, QUALITY-105) and match attribute names case-insensitively (the
|
||||
# live Woo attribute is 'Edge type', not 'Edge Type').
|
||||
out = {"species_leaves": [], "wood_category_leaves": [], "edge_terms": [], "figure_terms": [],
|
||||
"grade_terms": [], "thickness_terms": [], "moisture_terms": [], "feat_tags": []}
|
||||
"""Closed Call 1 lists only (#86 / METADATA-40).
|
||||
|
||||
Keys match the seed prompt. No grade/thickness/moisture (owner fields).
|
||||
Feature tag names are shown without the feat- prefix; ids are Woo/local tag ids.
|
||||
Groupers resolved by slug only (QUALITY-105).
|
||||
"""
|
||||
out: dict[str, list] = {
|
||||
"species": [],
|
||||
"wood_categories": [],
|
||||
"edge_terms": [],
|
||||
"figure_terms": [],
|
||||
"feature_tags": [],
|
||||
}
|
||||
# species = leaves under the 'species' grouper
|
||||
sp = conn.execute("SELECT id FROM woo_categories WHERE slug='species'").fetchone()
|
||||
if sp:
|
||||
for r in conn.execute(
|
||||
"SELECT id,name FROM woo_categories WHERE parent_id=?", (sp[0],)
|
||||
).fetchall():
|
||||
out["species_leaves"].append({"id": r[0], "name": r[1]})
|
||||
out["species"].append({"id": r[0], "name": r[1]})
|
||||
# wood categories = leaves under the 'wcat' grouper (leaves may sit under sub-groupers).
|
||||
wc = conn.execute("SELECT id FROM woo_categories WHERE slug='wcat'").fetchone()
|
||||
if wc:
|
||||
@@ -305,12 +387,8 @@ def _taxonomy_lists(conn) -> dict:
|
||||
for r in conn.execute(
|
||||
"SELECT id,name FROM woo_categories WHERE id IN (%s)" % q, leaf_ids
|
||||
).fetchall():
|
||||
out["wood_category_leaves"].append({"id": r[0], "name": r[1]})
|
||||
# attribute terms, case-insensitive on the attribute name. Grade stays in the synced set
|
||||
# (owner reference) but the model is told not to suggest it (METADATA-40).
|
||||
for attr, key in (("Edge type", "edge_terms"), ("Figure", "figure_terms"),
|
||||
("Grade", "grade_terms"), ("Thickness", "thickness_terms"),
|
||||
("Moisture", "moisture_terms")):
|
||||
out["wood_categories"].append({"id": r[0], "name": r[1]})
|
||||
for attr, key in (("Edge type", "edge_terms"), ("Figure", "figure_terms")):
|
||||
rows = conn.execute(
|
||||
"SELECT t.id,t.name FROM woo_attribute_terms t JOIN woo_attributes a ON t.attribute_id=a.id "
|
||||
"WHERE lower(a.name)=lower(?)",
|
||||
@@ -318,7 +396,7 @@ def _taxonomy_lists(conn) -> dict:
|
||||
).fetchall()
|
||||
out[key] = [{"id": r[0], "name": r[1]} for r in rows]
|
||||
for r in conn.execute("SELECT id,name FROM woo_tags WHERE slug LIKE 'feat-%'").fetchall():
|
||||
out["feat_tags"].append({"id": r[0], "name": r[1]})
|
||||
out["feature_tags"].append({"id": r[0], "name": _feat_display_name(r[1])})
|
||||
return out
|
||||
|
||||
|
||||
@@ -339,23 +417,24 @@ def _gate(parsed: dict) -> dict:
|
||||
|
||||
Confidence is read from the flat '<field>_confidence' keys; as a safety net it also
|
||||
understands the nested 'confidence: {<field>: n}' shape some models emit, so a shape
|
||||
wobble cannot silently nuke every suggestion. Grade is not suggested (METADATA-40).
|
||||
wobble cannot silently nuke every suggestion. Grade is never suggested (METADATA-40).
|
||||
Free-text observation bags are dropped — feature tags are ids only (#86).
|
||||
"""
|
||||
# Map each field to its confidence key. The field name and the confidence key do NOT
|
||||
# share a stem (e.g. field 'species_id' -> key 'species_confidence'), so derive it
|
||||
# explicitly rather than f"{field}_confidence".
|
||||
conf_key = {
|
||||
"species_id": "species_confidence",
|
||||
"wood_category_ids": "wood_category_confidence",
|
||||
"edge_type_term_id": "edge_confidence",
|
||||
"figure_term_ids": "figure_confidence",
|
||||
"feature_tag_ids": "feature_tag_confidence",
|
||||
}
|
||||
conf_obj = parsed.get("confidence") if isinstance(parsed.get("confidence"), dict) else {}
|
||||
|
||||
def _conf(field: str) -> Any:
|
||||
k = conf_key.get(field, f"{field}_confidence")
|
||||
if k in conf_obj:
|
||||
return conf_obj[k]
|
||||
return parsed.get(k)
|
||||
|
||||
def keep(field: str) -> Any:
|
||||
conf = _conf(field)
|
||||
if isinstance(conf, (int, float)) and conf >= _THRESHOLD and field in parsed:
|
||||
@@ -371,10 +450,9 @@ def _gate(parsed: dict) -> dict:
|
||||
out["edge_type_term_id"] = v
|
||||
if v := keep("figure_term_ids"):
|
||||
out["figure_term_ids"] = v
|
||||
# grade intentionally not gated/suggested (METADATA-40: owner judgment).
|
||||
for k in ("character", "inclusions", "voids", "checks"):
|
||||
if parsed.get(k):
|
||||
out[k] = parsed[k]
|
||||
if v := keep("feature_tag_ids"):
|
||||
out["feature_tag_ids"] = v
|
||||
# grade / free-text bags intentionally omitted (METADATA-40, #86).
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,101 @@ def test_prompt_seed_and_revision(env):
|
||||
assert client.get("/api/v1/prompts/call1").json()["active_content"] is not None
|
||||
|
||||
|
||||
def test_call1_seed_is_locked_expert_prompt(env):
|
||||
"""#86: shipped Call 1 seed is the locked expert prompt (no free-text bags, no grade)."""
|
||||
client, _conn = env
|
||||
from backend.inference import CALL1_SEED
|
||||
|
||||
body = client.get("/api/v1/prompts/call1").json()
|
||||
assert body["active_content"] == CALL1_SEED
|
||||
assert "wood identification expert" in CALL1_SEED
|
||||
assert "feature_tag_ids" in CALL1_SEED
|
||||
assert "Decide wood categories before edge type" in CALL1_SEED
|
||||
assert "highest-confidence" in CALL1_SEED
|
||||
assert "Wild edge" in CALL1_SEED
|
||||
assert "cookies and burl" in CALL1_SEED.lower() or "cookies and burl" in CALL1_SEED
|
||||
assert "grade_term_id" not in CALL1_SEED
|
||||
assert '"character"' not in CALL1_SEED
|
||||
assert '"inclusions"' not in CALL1_SEED
|
||||
assert "character\":" not in CALL1_SEED.replace(" ", "")
|
||||
|
||||
|
||||
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
|
||||
tmp = tempfile.mkdtemp(prefix="tax_")
|
||||
os.environ["SLAB_DB_PATH"] = os.path.join(tmp, "t.db")
|
||||
os.environ["SLAB_IMAGES_DIR"] = tmp
|
||||
os.environ.setdefault("WOO_BASE_URL", "https://x.com")
|
||||
os.environ.setdefault(
|
||||
"SLAB_AES_KEY",
|
||||
base64.b64encode(b"0123456789abcdef0123456789abcdef").decode(),
|
||||
)
|
||||
db_mod._reset_for_tests()
|
||||
conn = db_mod.init_db()
|
||||
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (10,'Species','species',NULL,10)")
|
||||
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (11,'Walnut','walnut',10,11)")
|
||||
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (20,'Wcat','wcat',NULL,20)")
|
||||
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (21,'Live edge wood','liveedge',20,21)")
|
||||
conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (6,'Edge type','pa_etype',6)")
|
||||
conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (8,'Figure','pa_fig',8)")
|
||||
conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (9,'Grade','pa_gr',9)")
|
||||
conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (10,'Thickness','pa_thk',10)")
|
||||
conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (7,'Moisture','pa_mst',7)")
|
||||
conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (321,6,'Square edge','sqe',321)")
|
||||
conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (328,8,'Plain','pl',328)")
|
||||
conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (342,9,'Fancy','fnc',342)")
|
||||
conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (350,10,'1\"- 1 1/2\"','4-6qtr',350)")
|
||||
conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (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 INTO woo_tags(id,name,slug,woo_id) VALUES (2,'fig-curl','fig-curl',2)")
|
||||
conn.commit()
|
||||
from backend.inference import _taxonomy_lists
|
||||
|
||||
tax = _taxonomy_lists(conn)
|
||||
assert set(tax.keys()) == {"species", "wood_categories", "edge_terms", "figure_terms", "feature_tags"}
|
||||
assert tax["species"] == [{"id": 11, "name": "Walnut"}]
|
||||
assert tax["wood_categories"] == [{"id": 21, "name": "Live edge wood"}]
|
||||
assert tax["edge_terms"] == [{"id": 321, "name": "Square edge"}]
|
||||
assert tax["figure_terms"] == [{"id": 328, "name": "Plain"}]
|
||||
# feat- prefix stripped for the model; fig- tags are not feature_tags
|
||||
assert tax["feature_tags"] == [{"id": 1, "name": "knot"}]
|
||||
assert "grade_terms" not in tax
|
||||
assert "thickness_terms" not in tax
|
||||
assert "moisture_terms" not in tax
|
||||
db_mod._reset_for_tests()
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def test_gate_feature_tag_ids_not_free_text():
|
||||
"""#86: gate keeps feature_tag_ids at threshold; drops free-text bags and grade."""
|
||||
from backend.inference import _gate
|
||||
|
||||
parsed = {
|
||||
"species_id": 11, "species_confidence": 0.9,
|
||||
"wood_category_ids": [21], "wood_category_confidence": 0.8,
|
||||
"edge_type_term_id": 321, "edge_confidence": 0.85,
|
||||
"figure_term_ids": [328, 335], "figure_confidence": 0.75,
|
||||
"feature_tag_ids": [1], "feature_tag_confidence": 0.8,
|
||||
"grade_term_id": 342, "grade_confidence": 0.99,
|
||||
"character": ["rustic"], "inclusions": ["bark"], "voids": [], "checks": ["end"],
|
||||
}
|
||||
out = _gate(parsed)
|
||||
assert out["species_id"] == 11
|
||||
assert out["wood_category_ids"] == [21]
|
||||
assert out["edge_type_term_id"] == 321
|
||||
assert out["figure_term_ids"] == [328, 335]
|
||||
assert out["feature_tag_ids"] == [1]
|
||||
assert "grade_term_id" not in out
|
||||
assert "character" not in out
|
||||
assert "inclusions" not in out
|
||||
assert "voids" not in out
|
||||
assert "checks" not in out
|
||||
# below threshold feature tags dropped
|
||||
low = {**parsed, "feature_tag_confidence": 0.2}
|
||||
assert "feature_tag_ids" not in _gate(low)
|
||||
|
||||
|
||||
def test_call1_confidence_gate(env, monkeypatch):
|
||||
client, conn = env
|
||||
# enable inference
|
||||
@@ -69,11 +164,18 @@ def test_call1_confidence_gate(env, monkeypatch):
|
||||
from backend.inference import InferenceClient
|
||||
|
||||
def fake_chat(self, messages):
|
||||
# Capture list header for #86 shape
|
||||
text = messages[0]["content"][-1]["text"]
|
||||
assert "LISTS:" in text
|
||||
assert "grade_terms" not in text
|
||||
assert "species_leaves" not in text
|
||||
return json.dumps({
|
||||
"species_id": 1, "species_confidence": 0.9,
|
||||
"wood_category_ids": [1], "wood_category_confidence": 0.5, # below threshold
|
||||
"grade_term_id": 7, "grade_confidence": 0.3, # below
|
||||
"grade_term_id": 7, "grade_confidence": 0.99, # never suggested
|
||||
"figure_term_ids": [2], "figure_confidence": 0.8, # at/above
|
||||
"feature_tag_ids": [9], "feature_tag_confidence": 0.9,
|
||||
"character": ["x"], "inclusions": ["y"],
|
||||
})
|
||||
monkeypatch.setattr(InferenceClient, "chat", fake_chat)
|
||||
r = client.post("/api/v1/slabs/s1/inference/call1")
|
||||
@@ -81,8 +183,10 @@ def test_call1_confidence_gate(env, monkeypatch):
|
||||
s = r.json()["suggestions"]
|
||||
assert s.get("species_id") == 1
|
||||
assert s.get("figure_term_ids") == [2]
|
||||
assert s.get("feature_tag_ids") == [9]
|
||||
assert "wood_category_ids" not in s # gated out
|
||||
assert "grade_term_id" not in s # gated out
|
||||
assert "grade_term_id" not in s # never
|
||||
assert "character" not in s
|
||||
# inference_log written with call=1
|
||||
assert conn.execute("SELECT call FROM inference_log").fetchone()["call"] == 1
|
||||
|
||||
|
||||
@@ -206,14 +206,18 @@ def test_gate_reads_flat_confidence_keys(client, db):
|
||||
# etc., NOT f'{field}_confidence' (field 'species_id' would give the wrong key). A
|
||||
# high-confidence response must surface its picks; low-confidence ones must not.
|
||||
from backend.inference import _gate, _parse_json
|
||||
good = '```json\n{"species_id": 284, "species_confidence": 0.85, "wood_category_ids": [251], "wood_category_confidence": 1.0, "edge_type_term_id": 323, "edge_confidence": 0.95, "figure_term_ids": [332], "figure_confidence": 0.9, "character": [], "inclusions": ["bark"], "voids": [], "checks": []}\n```'
|
||||
good = '```json\n{"species_id": 284, "species_confidence": 0.85, "wood_category_ids": [251], "wood_category_confidence": 1.0, "edge_type_term_id": 323, "edge_confidence": 0.95, "figure_term_ids": [332], "figure_confidence": 0.9, "feature_tag_ids": [1], "feature_tag_confidence": 0.8, "character": [], "inclusions": ["bark"], "voids": [], "checks": []}\n```'
|
||||
out = _gate(_parse_json(good))
|
||||
assert out["species_id"] == 284
|
||||
assert out["wood_category_ids"] == [251]
|
||||
assert out["edge_type_term_id"] == 323
|
||||
assert out["figure_term_ids"] == [332]
|
||||
assert out["feature_tag_ids"] == [1]
|
||||
# grade must never be gated in (METADATA-40)
|
||||
assert "grade_term_id" not in out
|
||||
# free-text bags dropped (#86)
|
||||
assert "character" not in out
|
||||
assert "inclusions" not in out
|
||||
# low-confidence fields are dropped (METADATA-45)
|
||||
low = '```json\n{"species_id": 284, "species_confidence": 0.4, "wood_category_ids": [251], "wood_category_confidence": 0.8, "edge_type_term_id": 323, "edge_confidence": 0.2, "figure_term_ids": [332], "figure_confidence": 0.9}\n```'
|
||||
out2 = _gate(_parse_json(low))
|
||||
|
||||
Reference in New Issue
Block a user