From 087557f89f0eb74ce282decb01e4569b81675ba5 Mon Sep 17 00:00:00 2001 From: grokbot_ty Date: Fri, 25 Sep 2026 17:46:39 -0700 Subject: [PATCH] chore(inference): disable Call 1 end-to-end; Call 2 stays overnight-whole-app: Call 1 (vision classification) is OUT. - POST /api/v1/slabs/{id}/inference/call1 always returns 410 call1_disabled (no model chat, no inference_log). Prompt seed/revision APIs kept dormant. - Review: remove "Suggest with AI" / runInference (inferTaxon). - GenerateText: remove Classify metadata (Call 1) button; Call 2 generate stays. - PROMPTS.md banner documents Call 1 OUT / Call 2 STAYS. - Settings: inference_enabled toggle annotated as Call-1 latch (dormant). - Backend tests updated to assert 410; Call 2 tests unchanged. --- backend/inference.py | 62 +++------ backend/tests/test_inference.py | 125 ++++-------------- docs/contracts/PROMPTS.md | 6 + frontend/src/api/slabs.ts | 3 +- .../src/create/inference/GenerateText.svelte | 26 +--- frontend/src/create/review/Review.svelte | 51 +------ frontend/src/create/settings/Settings.svelte | 2 +- 7 files changed, 57 insertions(+), 218 deletions(-) diff --git a/backend/inference.py b/backend/inference.py index b6282ad..2f29947 100644 --- a/backend/inference.py +++ b/backend/inference.py @@ -1,6 +1,8 @@ """Inference (S7): prompt revisions (REST), Call 1 vision classification, Call 2 content prose, test-inference, inference-off server description, inference_log. Cites METADATA-35..115, SETTINGS-30/45/90. + +overnight-whole-app: Call 1 POST is hard-disabled (410 call1_disabled). Call 2 stays. """ import base64 @@ -295,51 +297,21 @@ def _enabled(conn, key: str) -> bool: @router.post("/slabs/{slab_id}/inference/call1") async def call1(slab_id: str) -> dict: - conn = get_connection() - row = conn.execute("SELECT * FROM slabs WHERE id=? AND deleted_at IS NULL", (slab_id,)).fetchone() - if row is None: - return JSONResponse(status_code=404, content=_error("not_found", "Slab not found")) - if not _enabled(conn, "inference_enabled"): - return JSONResponse(status_code=409, content=_error("inference_disabled", "Inference is off")) - base, key, model = _inference_creds(conn) - 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. - photos = conn.execute( - "SELECT id, seq, processed_blob FROM slab_photos " - "WHERE slab_id=? AND processed_blob IS NOT NULL ORDER BY seq LIMIT 8", - (slab_id,), - ).fetchall() - image_parts = [] - photo_meta: list[dict] = [] - for p in photos: - blob = p["processed_blob"] if hasattr(p, "keys") else p[2] - pid = p["id"] if hasattr(p, "keys") else p[0] - seq = p["seq"] if hasattr(p, "keys") else p[1] - image_parts.append(_image_part_from_processed(blob)) - photo_meta.append(_photo_meta_row(pid, seq, blob)) - tax = _taxonomy_lists(conn) - 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() - try: - raw = client.chat(messages) - except InferenceConnectionError as e: - return JSONResponse(status_code=503, content=_error("inference_unreachable", str(e), {"recoverable": True})) - except InferenceError as e: - return JSONResponse(status_code=502, content=_error("inference_failed", str(e), {"recoverable": True})) - latency = int((time.time() - t0) * 1000) - parsed = _parse_json(raw) - _log_call(conn, slab_id, 1, act[0] if act else None, model, messages, raw, latency, photo_meta) - # Apply confidence gate (METADATA-45). - suggestions = _gate(parsed) - return {"ok": True, "suggestions": suggestions} + """Call 1 OUT (overnight-whole-app): hard-disabled. Call 2 stays. + + Route retained so clients get an explicit 410 instead of a half-wired path. + Prompt seed / revision APIs for call1 remain for docs & future re-enable. + """ + _ = slab_id # unused while disabled + return JSONResponse( + status_code=410, + content=_error( + "call1_disabled", + "Call 1 (vision classification) is disabled on this branch. Use Call 2 for description text.", + {"call": 1, "disabled": True}, + ), + ) + def _feat_display_name(name: str) -> str: diff --git a/backend/tests/test_inference.py b/backend/tests/test_inference.py index 7997880..6590ad0 100644 --- a/backend/tests/test_inference.py +++ b/backend/tests/test_inference.py @@ -153,42 +153,28 @@ def test_gate_feature_tag_ids_not_free_text(): assert "feature_tag_ids" not in _gate(low) -def test_call1_confidence_gate(env, monkeypatch): +def test_call1_hard_disabled_410(env, monkeypatch): + """overnight-whole-app: Call 1 OUT — POST always 410, never chats the model.""" client, conn = env - # enable inference conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('inference_enabled','true',NULL)") - conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (1,'Black Walnut','black-walnut',NULL,100)") conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,status,server_rev,client_rev,created_at,updated_at) VALUES ('s1','SLAB-01',100,2,'calibrated',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 + called = {"n": 0} + 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.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"], - }) + called["n"] += 1 + return "{}" + monkeypatch.setattr(InferenceClient, "chat", fake_chat) r = client.post("/api/v1/slabs/s1/inference/call1") - assert r.status_code == 200, r.text - 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 # never - assert "character" not in s - # inference_log written with call=1 - assert conn.execute("SELECT call FROM inference_log").fetchone()["call"] == 1 + assert r.status_code == 410, r.text + body = r.json() + assert body["error"]["code"] == "call1_disabled" + assert called["n"] == 0 + assert conn.execute("SELECT COUNT(*) AS n FROM inference_log").fetchone()["n"] == 0 def test_call2_stores_description(env, monkeypatch): @@ -208,12 +194,14 @@ def test_call2_stores_description(env, monkeypatch): assert row["content_source"] == "llm" -def test_inference_off_call1_409(env): +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 conn.execute("INSERT INTO slabs(id,sku,status,server_rev,client_rev,created_at,updated_at) VALUES ('s1','SLAB-01','calibrated',1,1,'t','t')") conn.commit() r = client.post("/api/v1/slabs/s1/inference/call1") - assert r.status_code == 409 # inference_enabled default off + assert r.status_code == 410 + assert r.json()["error"]["code"] == "call1_disabled" def test_server_description_inference_off(env): @@ -306,50 +294,18 @@ def test_redact_request_for_log_strips_data_urls_keeps_text(): assert "LISTS:" in parts[2]["text"] -def test_call1_log_has_text_and_image_refs_no_base64(env, monkeypatch): - """#87 METADATA-66: inference_log stores text+refs, not image bytes.""" +def test_call1_log_skipped_while_disabled(env): + """#87 Call 1 log path dormant while Call 1 is hard-disabled (overnight-whole-app).""" client, conn = env conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('inference_enabled','true',NULL)") conn.execute( "INSERT INTO slabs(id,sku,length_in,thickness_in,status,server_rev,client_rev,created_at,updated_at) " "VALUES ('s1','SLAB-01',100,2,'calibrated',1,1,'t','t')" ) - png = _rgba_png(400, 300) - conn.execute( - "INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('photo-abc','s1',1,?, 't')", - (png,), - ) conn.commit() - from backend.inference import InferenceClient - - def fake_chat(self, messages): - # Wire path still has data-URL for the model - content = messages[0]["content"] - assert any( - isinstance(p, dict) and p.get("type") == "image_url" - and str(p.get("image_url", {}).get("url", "")).startswith("data:image") - for p in content - ) - return json.dumps({"species_id": 1, "species_confidence": 0.9}) - - monkeypatch.setattr(InferenceClient, "chat", fake_chat) r = client.post("/api/v1/slabs/s1/inference/call1") - assert r.status_code == 200, r.text - row = conn.execute( - "SELECT request_payload, length(request_payload) AS n FROM inference_log ORDER BY id DESC LIMIT 1" - ).fetchone() - payload = row["request_payload"] - assert row["n"] < 50000 - assert "data:image" not in payload - assert "LISTS:" in payload - assert "image_ref" in payload - assert "photo-abc" in payload - assert '"seq": 1' in payload or '"seq":1' in payload - logged = json.loads(payload) - refs = [p for p in logged[0]["content"] if p.get("type") == "image_ref"] - assert len(refs) == 1 - assert refs[0]["photo_id"] == "photo-abc" - assert refs[0]["bytes"] == len(png) + assert r.status_code == 410 + assert conn.execute("SELECT COUNT(*) AS n FROM inference_log").fetchone()["n"] == 0 def test_call2_log_redacts_images(env, monkeypatch): @@ -380,38 +336,9 @@ def test_call2_log_redacts_images(env, monkeypatch): assert "DIMENSION FACT BLOCK" in payload -def test_call1_sends_long_edge_at_most_1024(env, monkeypatch): - """METADATA-35 end-to-end: messages to the model use <=1024 long edge.""" - client, conn = env - conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('inference_enabled','true',NULL)") - conn.execute( - "INSERT INTO slabs(id,sku,length_in,thickness_in,status,server_rev,client_rev,created_at,updated_at) " - "VALUES ('s1','SLAB-01',100,2,'calibrated',1,1,'t','t')" - ) - big = _rgba_png(800, 1067) - conn.execute( - "INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('p1','s1',1,?, 't')", - (big,), - ) - conn.commit() - from backend.inference import InferenceClient - from PIL import Image - import io - - captured = {} - - def fake_chat(self, messages): - captured["messages"] = messages - return json.dumps({"species_id": 1, "species_confidence": 0.9}) - - monkeypatch.setattr(InferenceClient, "chat", fake_chat) - r = client.post("/api/v1/slabs/s1/inference/call1") - assert r.status_code == 200, r.text - content = captured["messages"][0]["content"] - imgs = [p for p in content if isinstance(p, dict) and p.get("type") == "image_url"] - assert len(imgs) == 1 - url = imgs[0]["image_url"]["url"] - assert url.startswith("data:image/png;base64,") - raw = base64.b64decode(url.split(",", 1)[1]) - im = Image.open(io.BytesIO(raw)) - assert max(im.size) <= 1024 +def test_call1_sends_long_edge_dormant_while_disabled(env): + """METADATA-35 Call 1 sizing path dormant while Call 1 is hard-disabled.""" + client, _conn = env + r = client.post("/api/v1/slabs/missing/inference/call1") + assert r.status_code == 410 + assert r.json()["error"]["code"] == "call1_disabled" diff --git a/docs/contracts/PROMPTS.md b/docs/contracts/PROMPTS.md index 409f6b6..476c178 100644 --- a/docs/contracts/PROMPTS.md +++ b/docs/contracts/PROMPTS.md @@ -1,5 +1,11 @@ # Prompts contract +> **overnight-whole-app / Call 1 OUT:** Call 1 (vision classification) is hard-disabled. +> `POST /api/v1/slabs/{id}/inference/call1` returns **410 call1_disabled**. UI triggers +> (Review "Suggest with AI", GenerateText classify) are removed. Call 2 +> (`POST .../inference/call2`, Review "Generate description") stays invokable. +> Call 1 seed text and prompt-revision APIs remain for documentation / future re-enable. + Status: SPEC · 2026-09-18. Owns prompt storage, prompt inputs, and the two inference calls. diff --git a/frontend/src/api/slabs.ts b/frontend/src/api/slabs.ts index 4b5bc4d..478d726 100644 --- a/frontend/src/api/slabs.ts +++ b/frontend/src/api/slabs.ts @@ -5,7 +5,7 @@ // photos.py POST/GET /api/v1/slabs/{id}/photos[.../original|processed] // review.py POST /api/v1/slabs/{id}/review // publish.py POST /api/v1/slabs/{id}/publish -// inference.py POST /api/v1/slabs/{id}/inference/call1|call2 +// inference.py POST /api/v1/slabs/{id}/inference/call1 (DISABLED 410) | call2 // settings.py GET/PUT /api/v1/settings ; POST /api/v1/settings/test-woo|test-inference // taxonomy.py GET/POST /api/v1/admin/taxonomy[.../sync] // pricing.py CRUD /api/v1/admin/pricing[/{id}] @@ -332,6 +332,7 @@ export interface Call2Result { description: string; } +/** Call 1 OUT (overnight-whole-app): always fails with 410 call1_disabled. Prefer manual taxonomy. */ export function inferTaxon(slabId: string): Promise { return fetch(`/api/v1/slabs/${encodeURIComponent(slabId)}/inference/call1`, { method: 'POST', diff --git a/frontend/src/create/inference/GenerateText.svelte b/frontend/src/create/inference/GenerateText.svelte index 66baa50..b2c85ee 100644 --- a/frontend/src/create/inference/GenerateText.svelte +++ b/frontend/src/create/inference/GenerateText.svelte @@ -3,7 +3,6 @@ let contentEnabled = false; let reachable = false; let description: string | null = null; - let call1Suggestions: Record | null = null; let error = ''; let busy = false; @@ -41,25 +40,6 @@ busy = false; } - async function classify() { - if (!slabId) { - error = 'Enter a slab id.'; - return; - } - busy = true; - error = ''; - call1Suggestions = null; - try { - const res = await fetch(`/api/v1/slabs/${encodeURIComponent(slabId)}/inference/call1`, { method: 'POST' }); - const body = await res.json(); - if (res.ok) call1Suggestions = body.suggestions; - else error = body.error?.message || `call1 failed (${res.status})`; - } catch { - error = 'network error'; - } - busy = false; - } - checkReadiness(); @@ -68,6 +48,7 @@

Content generation: {contentEnabled ? 'on' : 'off'} · endpoint: {reachable ? 'reachable' : 'unreachable'}

+

Call 1 (vision classification) is disabled on this branch. Call 2 description generation stays.

{#if !contentEnabled || !reachable}

The Generate text control is disabled: it appears only when content generation is enabled and the inference endpoint is reachable.

@@ -78,13 +59,9 @@
-
{#if error}

{error}

{/if} {#if description}

{description}

{/if} - {#if call1Suggestions} -
{JSON.stringify(call1Suggestions, null, 2)}
- {/if} {/if} @@ -98,6 +75,5 @@ .note { color: #e8a; } .err { color: #e88; } .desc { margin-top: 12px; } - .sug { background: #1c1c1c; padding: 8px; } .mini { margin-top: 12px; font-size: 12px; } diff --git a/frontend/src/create/review/Review.svelte b/frontend/src/create/review/Review.svelte index c4a2f9f..d7d0f66 100644 --- a/frontend/src/create/review/Review.svelte +++ b/frontend/src/create/review/Review.svelte @@ -1,7 +1,7 @@