fix(products): work around Gemini int-enum schema rejection in parse-text
Gemini API rejects int-valued enums (StrengthLevel) in response_schema, raising a validation error before any request is sent. Fix by introducing AIActiveIngredient (inherits ActiveIngredient, overrides strength_level and irritation_potential as Optional[int]) and ProductParseLLMResponse used only as the Gemini schema. The two-step validation converts ints back to StrengthLevel via Pydantic coercion. Adds a test covering the numeric strength level path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
921fe3ef61
commit
914c6087bd
2 changed files with 35 additions and 2 deletions
|
|
@ -143,6 +143,19 @@ class ProductParseResponse(SQLModel):
|
||||||
needle_length_mm: Optional[float] = None
|
needle_length_mm: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AIActiveIngredient(ActiveIngredient):
|
||||||
|
# Gemini API rejects int-enum values in response_schema; override with plain int.
|
||||||
|
strength_level: Optional[int] = None
|
||||||
|
irritation_potential: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ProductParseLLMResponse(ProductParseResponse):
|
||||||
|
# Gemini response schema currently requires enum values to be strings.
|
||||||
|
# Strength fields are numeric in our domain (1-3), so keep them as ints here
|
||||||
|
# and convert via ProductParseResponse validation afterward.
|
||||||
|
actives: Optional[list[AIActiveIngredient]] = None
|
||||||
|
|
||||||
|
|
||||||
class InventoryCreate(SQLModel):
|
class InventoryCreate(SQLModel):
|
||||||
is_opened: bool = False
|
is_opened: bool = False
|
||||||
opened_at: Optional[date] = None
|
opened_at: Optional[date] = None
|
||||||
|
|
@ -373,7 +386,7 @@ def parse_product_text(data: ProductParseRequest) -> ProductParseResponse:
|
||||||
config=genai_types.GenerateContentConfig(
|
config=genai_types.GenerateContentConfig(
|
||||||
system_instruction=_product_parse_system_prompt(),
|
system_instruction=_product_parse_system_prompt(),
|
||||||
response_mime_type="application/json",
|
response_mime_type="application/json",
|
||||||
response_schema=ProductParseResponse,
|
response_schema=ProductParseLLMResponse,
|
||||||
max_output_tokens=16384,
|
max_output_tokens=16384,
|
||||||
temperature=0.0,
|
temperature=0.0,
|
||||||
),
|
),
|
||||||
|
|
@ -387,7 +400,8 @@ def parse_product_text(data: ProductParseRequest) -> ProductParseResponse:
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
raise HTTPException(status_code=502, detail=f"LLM returned invalid JSON: {e}")
|
raise HTTPException(status_code=502, detail=f"LLM returned invalid JSON: {e}")
|
||||||
try:
|
try:
|
||||||
return ProductParseResponse.model_validate(parsed)
|
llm_parsed = ProductParseLLMResponse.model_validate(parsed)
|
||||||
|
return ProductParseResponse.model_validate(llm_parsed.model_dump())
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
raise HTTPException(status_code=422, detail=e.errors())
|
raise HTTPException(status_code=422, detail=e.errors())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -199,3 +199,22 @@ def test_create_inventory(client, created_product):
|
||||||
def test_create_inventory_product_not_found(client):
|
def test_create_inventory_product_not_found(client):
|
||||||
r = client.post(f"/products/{uuid.uuid4()}/inventory", json={})
|
r = client.post(f"/products/{uuid.uuid4()}/inventory", json={})
|
||||||
assert r.status_code == 404
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_text_accepts_numeric_strength_levels(client, monkeypatch):
|
||||||
|
from innercontext.api import products as products_api
|
||||||
|
|
||||||
|
class _FakeResponse:
|
||||||
|
text = (
|
||||||
|
'{"name":"Test Serum","actives":[{"name":"Niacinamide","percent":10,'
|
||||||
|
'"functions":["niacinamide"],"strength_level":2,"irritation_potential":1}]}'
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(products_api, "call_gemini", lambda **kwargs: _FakeResponse())
|
||||||
|
|
||||||
|
r = client.post("/products/parse-text", json={"text": "dummy input"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
data = r.json()
|
||||||
|
assert data["name"] == "Test Serum"
|
||||||
|
assert data["actives"][0]["strength_level"] == 2
|
||||||
|
assert data["actives"][0]["irritation_potential"] == 1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue