3 Commits

Author SHA1 Message Date
Joseph Doherty 49be3cf4b9 fix: parse_turn falls back gracefully + classify logs flapping classifiers
The turn endpoint was 500ing in multi-bot scenes whenever the
classifier provider hiccuped on parse_turn — particularly visible
after a guest was added and bots started exchanging turns. The
traceback was 'classify failed for schema ParsedTurn with no default'
because parse_turn was the only classify caller without a default.

Two changes:

- chat/services/turn_parse.py: parse_turn now passes a default that
  wraps the whole prose as one 'dialogue' segment. The narrative
  still fires on the prose; we lose finer-grained segment kinds
  (action vs dialogue vs ooc) on this turn, but the request returns
  cleanly. Updated the existing test that pinned the old
  RuntimeError contract.

- chat/llm/classify.py: when retries are exhausted, log a WARNING
  with the schema name, last error type, and a snippet of the last
  raw text the model returned. Surfaces flapping classifiers in the
  uvicorn log for diagnosis without taking down the request.

Suite: 471 passed in 11.7s.
2026-04-27 15:07:39 -04:00
Joseph Doherty 5aab98e4d7 fix: classifier robustness — schema in prompt, retries, kickoff fallback
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.
2026-04-26 15:03:13 -04:00
Joseph Doherty a0f5e818ec feat: turn input parser via classifier 2026-04-26 12:53:34 -04:00