fix(wizard): 503 on LLM error, sanitize history content, typed HistoryMessage model

This commit is contained in:
pyr0ball 2026-06-13 20:04:14 -07:00
parent 6d1edff1b9
commit e9943908c6
2 changed files with 20 additions and 16 deletions

View file

@ -4541,8 +4541,13 @@ You must ALWAYS respond with valid JSON in this exact format:
Only include fields in extracted_fields that you are confident about from the conversation. Do not include fields the user hasn't mentioned. Infer complete=true when all required fields (name, email, career_summary) are gathered or when user explicitly says done.""" Only include fields in extracted_fields that you are confident about from the conversation. Do not include fields the user hasn't mentioned. Infer complete=true when all required fields (name, email, career_summary) are gathered or when user explicitly says done."""
class HistoryMessage(BaseModel):
role: str # "user" or "assistant"
content: str
class WizardInterviewRequest(BaseModel): class WizardInterviewRequest(BaseModel):
history: list[dict] # [{"role": "user"|"assistant", "content": "..."}] history: list[HistoryMessage] = []
profile_so_far: dict = {} profile_so_far: dict = {}
@ -4574,8 +4579,8 @@ def wizard_ai_interview(request: WizardInterviewRequest):
# Build conversation prompt from history # Build conversation prompt from history
conversation_lines = [] conversation_lines = []
for msg in request.history: for msg in request.history:
role = msg.get("role", "user") role = msg.role
content = msg.get("content", "") content = msg.content.replace("\n", " ").replace("\r", "")
if role == "user": if role == "user":
conversation_lines.append(f"User: {content}") conversation_lines.append(f"User: {content}")
else: else:
@ -4600,7 +4605,7 @@ def wizard_ai_interview(request: WizardInterviewRequest):
from scripts.llm_router import LLMRouter from scripts.llm_router import LLMRouter
response_text = LLMRouter().complete(prompt, system=_AI_WIZARD_SYSTEM_PROMPT) response_text = LLMRouter().complete(prompt, system=_AI_WIZARD_SYSTEM_PROMPT)
except Exception as exc: except Exception as exc:
raise HTTPException(500, detail={"error": "llm_error", "message": str(exc)}) raise HTTPException(503, detail={"error": "llm_error", "message": str(exc)})
try: try:
parsed = json.loads(response_text) parsed = json.loads(response_text)
@ -4617,15 +4622,14 @@ def wizard_ai_interview(request: WizardInterviewRequest):
def wizard_ai_finalize(request: WizardFinalizeRequest): def wizard_ai_finalize(request: WizardFinalizeRequest):
"""Merge AI-collected wizard fields into user.yaml. Only allowed fields are written.""" """Merge AI-collected wizard fields into user.yaml. Only allowed fields are written."""
yaml_path = _user_yaml_path() yaml_path = _user_yaml_path()
try:
current = load_user_profile(yaml_path) current = load_user_profile(yaml_path)
updates = {k: v for k, v in request.profile.items() if k in _WIZARD_ALLOWED_FIELDS}
merged_keys = [] merged = {**current, **updates}
for key, value in request.profile.items(): save_user_profile(yaml_path, merged)
if key in _WIZARD_ALLOWED_FIELDS: except Exception as exc:
current[key] = value raise HTTPException(500, detail={"error": "write_error", "message": str(exc)})
merged_keys.append(key) merged_keys = list(updates.keys())
save_user_profile(yaml_path, current)
return {"saved": True, "fields": merged_keys} return {"saved": True, "fields": merged_keys}

View file

@ -226,8 +226,8 @@ class TestWizardAIInterviewLLM:
assert "Alex Rivera" in prompt assert "Alex Rivera" in prompt
assert "alex@example.com" in prompt assert "alex@example.com" in prompt
def test_llm_error_returns_500(self, client): def test_llm_error_returns_503(self, client):
"""If LLM raises, the endpoint returns 500.""" """If LLM raises, the endpoint returns 503."""
with patch("dev_api._get_effective_tier", return_value="paid"): with patch("dev_api._get_effective_tier", return_value="paid"):
with patch("app.wizard.tiers.has_configured_llm", return_value=True): with patch("app.wizard.tiers.has_configured_llm", return_value=True):
with patch("scripts.llm_router.LLMRouter") as mock_cls: with patch("scripts.llm_router.LLMRouter") as mock_cls:
@ -236,7 +236,7 @@ class TestWizardAIInterviewLLM:
"/api/wizard/ai/interview", "/api/wizard/ai/interview",
json={"history": [{"role": "user", "content": "hi"}]}, json={"history": [{"role": "user", "content": "hi"}]},
) )
assert r.status_code == 500 assert r.status_code == 503
# ── POST /api/wizard/ai/finalize ────────────────────────────────────────────── # ── POST /api/wizard/ai/finalize ──────────────────────────────────────────────