refactor(llm): use response_schema with typed enums in all Gemini calls
Pass response_schema to all three generate_content calls so Gemini constrains its output to valid enum values and correct JSON structure: - routines.py: _StepOut.action_type Optional[str] → Optional[GroomingAction] - skincare.py: add _SkinAnalysisOut(PydanticBase) with OverallSkinState, SkinType, SkinTexture, BarrierState, SkinConcern enums; add response_schema - products.py: pass ProductParseResponse directly as response_schema; remove NaN/Infinity/undefined regex cleanup, markdown-fence extraction, finish_reason logging, and re import — all now unnecessary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6e7f715ef2
commit
81b1cacc5c
3 changed files with 20 additions and 27 deletions
|
|
@ -1,12 +1,8 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from google.genai import types as genai_types
|
from google.genai import types as genai_types
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
@ -378,37 +374,17 @@ 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,
|
||||||
max_output_tokens=16384,
|
max_output_tokens=16384,
|
||||||
temperature=0.0,
|
temperature=0.0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
candidate = response.candidates[0] if response.candidates else None
|
|
||||||
finish_reason = str(candidate.finish_reason) if candidate else "unknown"
|
|
||||||
raw = response.text
|
raw = response.text
|
||||||
if not raw:
|
if not raw:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=502, detail="LLM returned an empty response")
|
||||||
status_code=502,
|
|
||||||
detail=f"LLM returned an empty response (finish_reason={finish_reason})",
|
|
||||||
)
|
|
||||||
# Fallback: extract JSON object in case the model adds preamble or markdown fences
|
|
||||||
if not raw.lstrip().startswith("{"):
|
|
||||||
start = raw.find("{")
|
|
||||||
end = raw.rfind("}")
|
|
||||||
if start != -1 and end != -1:
|
|
||||||
raw = raw[start : end + 1]
|
|
||||||
# Replace JS-style non-JSON literals that some models emit
|
|
||||||
raw = re.sub(r":\s*NaN\b", ": null", raw)
|
|
||||||
raw = re.sub(r":\s*Infinity\b", ": null", raw)
|
|
||||||
raw = re.sub(r":\s*undefined\b", ": null", raw)
|
|
||||||
try:
|
try:
|
||||||
parsed = json.loads(raw)
|
parsed = json.loads(raw)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
log.error(
|
|
||||||
"Gemini parse-text JSON error at pos %d finish_reason=%s context=%r",
|
|
||||||
e.pos,
|
|
||||||
finish_reason,
|
|
||||||
raw[max(0, e.pos - 80) : e.pos + 80],
|
|
||||||
)
|
|
||||||
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)
|
return ProductParseResponse.model_validate(parsed)
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ class BatchSuggestion(SQLModel):
|
||||||
|
|
||||||
class _StepOut(PydanticBase):
|
class _StepOut(PydanticBase):
|
||||||
product_id: Optional[str] = None
|
product_id: Optional[str] = None
|
||||||
action_type: Optional[str] = None
|
action_type: Optional[GroomingAction] = None
|
||||||
dose: Optional[str] = None
|
dose: Optional[str] = None
|
||||||
region: Optional[str] = None
|
region: Optional[str] = None
|
||||||
action_notes: Optional[str] = None
|
action_notes: Optional[str] = None
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from uuid import UUID, uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||||
from google.genai import types as genai_types
|
from google.genai import types as genai_types
|
||||||
|
from pydantic import BaseModel as PydanticBase
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from sqlmodel import Session, SQLModel, select
|
from sqlmodel import Session, SQLModel, select
|
||||||
|
|
||||||
|
|
@ -70,6 +71,21 @@ class SkinPhotoAnalysisResponse(SQLModel):
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class _SkinAnalysisOut(PydanticBase):
|
||||||
|
overall_state: Optional[OverallSkinState] = None
|
||||||
|
skin_type: Optional[SkinType] = None
|
||||||
|
texture: Optional[SkinTexture] = None
|
||||||
|
hydration_level: Optional[int] = None
|
||||||
|
sebum_tzone: Optional[int] = None
|
||||||
|
sebum_cheeks: Optional[int] = None
|
||||||
|
sensitivity_level: Optional[int] = None
|
||||||
|
barrier_state: Optional[BarrierState] = None
|
||||||
|
active_concerns: Optional[list[SkinConcern]] = None
|
||||||
|
risks: Optional[list[str]] = None
|
||||||
|
priorities: Optional[list[str]] = None
|
||||||
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -148,6 +164,7 @@ async def analyze_skin_photos(
|
||||||
config=genai_types.GenerateContentConfig(
|
config=genai_types.GenerateContentConfig(
|
||||||
system_instruction=_skin_photo_system_prompt(),
|
system_instruction=_skin_photo_system_prompt(),
|
||||||
response_mime_type="application/json",
|
response_mime_type="application/json",
|
||||||
|
response_schema=_SkinAnalysisOut,
|
||||||
max_output_tokens=2048,
|
max_output_tokens=2048,
|
||||||
temperature=0.0,
|
temperature=0.0,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue