5aab98e4d7
The kickoff parse-and-confirm route was 500-ing intermittently because
Hermes-3 + Featherless's response_format={"type":"json_object"} only
guarantees JSON output, NOT a particular schema. The model was inventing
its own field names (sceneTime, entities, settingDetails) instead of
the KickoffParse fields, causing Pydantic validation to fail on both
classify() retries.
Three changes:
1. Include the Pydantic JSON schema in the system prompt so the model
knows exactly which keys to produce. Affects every classify() call
(kickoff parse, turn parse, scene-close detect, significance,
state-update, scene summarize). Strip ```json fences if the model
wraps its output. Bump retries 2 → 3 (model is stochastic; one extra
attempt closes most of the remaining gap).
2. parse_kickoff() now passes a default empty KickoffParse so the
route degrades to a fillable form instead of 500 when the classifier
ultimately fails. The confirm form is the human-in-the-loop; an
empty form is strictly better UX than a stack trace.
3. Tests updated: bumped canned-failure arrays from 2 → 3 entries to
match the new attempt count; renamed kickoff test from
"raises_when_classifier_fails_twice" to
"falls_back_to_empty_when_classifier_fails" reflecting the new
degraded-but-usable behavior.
Verified live with all 3 sample bots (maya/eli/sam) — kickoff route
returns 200 across multiple attempts. Full suite: 168 passed.
25 lines
776 B
Python
25 lines
776 B
Python
import pytest
|
|
from pydantic import BaseModel
|
|
from chat.llm.mock import MockLLMClient
|
|
from chat.llm.classify import classify
|
|
|
|
|
|
class Verdict(BaseModel):
|
|
score: int
|
|
reason: str
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_classify_parses_valid_json():
|
|
mock = MockLLMClient(canned=['{"score": 2, "reason": "notable"}'])
|
|
result = await classify(mock, model="m", system="x", user="y", schema=Verdict)
|
|
assert result.score == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_classify_falls_back_on_unparseable_after_retry():
|
|
mock = MockLLMClient(canned=["nope", "still nope", "nope3"])
|
|
default = Verdict(score=1, reason="fallback")
|
|
result = await classify(mock, model="m", system="x", user="y", schema=Verdict, default=default)
|
|
assert result.reason == "fallback"
|