[Game Systems] Schema-targeted stat-block generation (upgrade of #130) #142

Closed
opened 2026-07-15 22:02:20 +00:00 by claude-bot · 2 comments
Contributor

Motivation/Context

#130 ("One-click 'Flesh out' NPC backstory and stat block", v3.7.0) builds a
generate_statblock draft mode that is deliberately system-agnostic: "LLM
free-form guided by the game_system hint," storing results in body markdown plus
loose sidebar_fields rows. #130's own text names this investigation as the intended
follow-on: "If game-aware work lands, the stat-block generator can target a real
schema. Storing stats as structured sidebar_fields is chosen partly to smooth that
transition."
With the versioned schema + validated stats envelope from #139 in
place, this issue delivers that upgrade: when a campaign has a linked system with an
active schema, generate_statblock targets it directly instead of producing free-form
prose to parse later.

Sequencing note (per the report, §3.3): #130 should ship system-agnostic first —
its draft-rail plumbing (generating→ready→failed lifecycle, review/iterate/approve UI,
versioning) is the reusable part. This issue is a bolt-on, not a redesign of #130.

Approach

When the campaign has a linked game_system_id with an active schema for the entry's
LoreType, generate_statblock mode (added by #130 in
webapp/backend/app/services/audio_service.py, modeled on rephrase_lore_entry_body
~line 717) passes the schema's field list into generate_structured_text(…, json_mode=True) (audio_service.py:1158) and asks the LLM for a values object
matching the schema's field keys/types/groups — rather than free-form markdown-only
output. The response is validated against the schema (via the validator service from
#139) before entering the draft; a failing validation retries once with the error
appended as guidance, then falls back to #130's current free-form behaviour if it still
doesn't validate.

On approval, write both:

  • the schema-conformant values into the entry's stats envelope (per #139's storage
    design), and
  • the existing markdown/sidebar_fields outputs #130 already produces, so the
    human-readable stat block in body stays in sync with the structured data (per
    #130's "coherence requirement" — stats must reflect the backstory, and now also the
    schema).

Fallback path: campaigns with no linked system, or a linked system with no active
schema (e.g. generic), or a schema that fails to validate after retry, get exactly
#130's existing free-form behaviour unchanged. This upgrade is strictly additive on top
of #130 — no regression for free-form campaigns.

Draft review UX (old-vs-new diff, inline edit, iterate-with-feedback, approve/discard)
is #130's existing rail and is not rebuilt here; this issue only changes what the
generation step produces and validates when a schema is available.

Dependencies

  • #130 (One-click "Flesh out" NPC backstory and stat block, v3.7.0) — must ship first;
    this issue upgrades its generate_statblock mode rather than building it from
    scratch.
  • #139 (Versioned stat schemas + stats storage + validation service) — provides the
    schema definitions to target and the validator to check the LLM's output against.

Out of scope

  • #130's core draft-rail plumbing, backstory generation, or UI — unchanged, reused as-is.
  • The stat-block editor (#140) — this issue is generation only, not manual editing.
  • The conversion wizard's convert_stats mode (issue 7) — that backfills existing
    entries; this issue is about newly generated stat blocks.
  • Any rules-engine validation beyond schema field types/ranges/enums.

Acceptance criteria

  • For a campaign with a linked system and active schema, "Generate stat block" produces
    a draft whose structured output validates against that schema's current version.
  • On approval, the entry's stats envelope is written with the correct system key
    and schema_version, and body/sidebar_fields are updated exactly as #130
    specifies today (markdown + infobox rows).
  • For a campaign with no linked system, an unschemaed system (generic), or a
    validation failure that survives one retry, generation falls back to #130's current
    free-form behaviour with no visible regression.
  • Existing #130 tests continue to pass unmodified for the free-form path; new tests
    cover the schema-targeted path (successful validation, one retry-then-fallback case).
  • Stat-block generation still consumes the current backstory as context (per #130's
    coherence requirement), now also passing the schema field list.
## Motivation/Context #130 ("One-click 'Flesh out' NPC backstory and stat block", v3.7.0) builds a `generate_statblock` draft mode that is deliberately **system-agnostic**: "LLM free-form guided by the `game_system` hint," storing results in `body` markdown plus loose `sidebar_fields` rows. #130's own text names this investigation as the intended follow-on: *"If game-aware work lands, the stat-block generator can target a real schema. Storing stats as structured `sidebar_fields` is chosen partly to smooth that transition."* With the versioned schema + validated `stats` envelope from #139 in place, this issue delivers that upgrade: when a campaign has a linked system with an active schema, `generate_statblock` targets it directly instead of producing free-form prose to parse later. **Sequencing note (per the report, §3.3):** #130 should ship system-agnostic first — its draft-rail plumbing (generating→ready→failed lifecycle, review/iterate/approve UI, versioning) is the reusable part. This issue is a bolt-on, not a redesign of #130. ## Approach When the campaign has a linked `game_system_id` with an active schema for the entry's `LoreType`, `generate_statblock` mode (added by #130 in `webapp/backend/app/services/audio_service.py`, modeled on `rephrase_lore_entry_body` ~line 717) passes the schema's field list into `generate_structured_text(…, json_mode=True)` (`audio_service.py:1158`) and asks the LLM for a `values` object matching the schema's field keys/types/groups — rather than free-form markdown-only output. The response is validated against the schema (via the validator service from #139) before entering the draft; a failing validation retries once with the error appended as guidance, then falls back to #130's current free-form behaviour if it still doesn't validate. **On approval**, write both: - the schema-conformant `values` into the entry's `stats` envelope (per #139's storage design), and - the existing markdown/`sidebar_fields` outputs #130 already produces, so the human-readable stat block in `body` stays in sync with the structured data (per #130's "coherence requirement" — stats must reflect the backstory, and now also the schema). **Fallback path:** campaigns with no linked system, or a linked system with no active schema (e.g. `generic`), or a schema that fails to validate after retry, get exactly #130's existing free-form behaviour unchanged. This upgrade is strictly additive on top of #130 — no regression for free-form campaigns. Draft review UX (old-vs-new diff, inline edit, iterate-with-feedback, approve/discard) is #130's existing rail and is not rebuilt here; this issue only changes what the generation step produces and validates when a schema is available. ## Dependencies - #130 (One-click "Flesh out" NPC backstory and stat block, v3.7.0) — must ship first; this issue upgrades its `generate_statblock` mode rather than building it from scratch. - #139 (Versioned stat schemas + `stats` storage + validation service) — provides the schema definitions to target and the validator to check the LLM's output against. ## Out of scope - #130's core draft-rail plumbing, backstory generation, or UI — unchanged, reused as-is. - The stat-block editor (#140) — this issue is generation only, not manual editing. - The conversion wizard's `convert_stats` mode (issue 7) — that backfills existing entries; this issue is about newly generated stat blocks. - Any rules-engine validation beyond schema field types/ranges/enums. ## Acceptance criteria - For a campaign with a linked system and active schema, "Generate stat block" produces a draft whose structured output validates against that schema's current version. - On approval, the entry's `stats` envelope is written with the correct `system` key and `schema_version`, and `body`/`sidebar_fields` are updated exactly as #130 specifies today (markdown + infobox rows). - For a campaign with no linked system, an unschemaed system (`generic`), or a validation failure that survives one retry, generation falls back to #130's current free-form behaviour with no visible regression. - Existing #130 tests continue to pass unmodified for the free-form path; new tests cover the schema-targeted path (successful validation, one retry-then-fallback case). - Stat-block generation still consumes the current backstory as context (per #130's coherence requirement), now also passing the schema field list.
Author
Contributor

Picking this up on feat/142-schema-targeted-statblock (PR to target the feat/v3.9-game-aware-systems integration branch). #130 (free-form statblock) and #139 (schemas + stats) are both merged into the base.

Approach: when a campaign links a system whose active schema covers the entry's type, the existing generate_statblock draft targets that schema — generate_structured_text(json_mode=True) for a schema-conforming values object, validated via #139's validator (retry once with the error as guidance, then fall back to #130's free-form path). The proposed stats envelope is staged on the draft (new lore_entry_drafts.current_stats column) and written to entry.stats on approval, alongside #130's markdown/sidebar_fields (derived from the same values for coherence). Also folds in #137's flagged follow-up — the statblock/backstory prompts switch to resolve_system_prompt_context. No-schema/generic campaigns keep #130's behaviour unchanged. No bot API change.

Picking this up on `feat/142-schema-targeted-statblock` (PR to target the `feat/v3.9-game-aware-systems` integration branch). #130 (free-form statblock) and #139 (schemas + `stats`) are both merged into the base. Approach: when a campaign links a system whose active schema covers the entry's type, the existing `generate_statblock` draft targets that schema — `generate_structured_text(json_mode=True)` for a schema-conforming `values` object, validated via #139's validator (retry once with the error as guidance, then fall back to #130's free-form path). The proposed `stats` envelope is staged on the draft (new `lore_entry_drafts.current_stats` column) and written to `entry.stats` on approval, alongside #130's markdown/sidebar_fields (derived from the same values for coherence). Also folds in #137's flagged follow-up — the statblock/backstory prompts switch to `resolve_system_prompt_context`. No-schema/generic campaigns keep #130's behaviour unchanged. No bot API change.
Author
Contributor

Done and verified — merged into the integration branch via PR #210.

Verification (Docker, py3.12): 641 backend tests pass (+9 new in test_statblock_schema_targeting.py), no regressions. Migration d8e9f0a1b2c3 up/down/up clean; current_stats column added.

Delivered: schema-targeted generate_statblock (generate_lore_entry_statblock_targeted → schema-conforming values, validate-retry-then-fallback to #130's free-form path, sidebar/body derived from the same values for coherence); the proposed envelope staged on lore_entry_drafts.current_stats and written to entry.stats on approval (re-validated against the current schema). No-schema/generic campaigns keep #130's behaviour unchanged.

Also resolved #137's flagged follow-up — the flesh-out backstory/statblock prompts now use resolve_system_prompt_context.

Follow-up flagged: patch_draft doesn't yet accept current_stats edits during review (a GM editing a staged sidebar row won't retro-sync the structured envelope) — small follow-up if inline stat editing is wanted.

Closing; ships to main with the v3.9.0 release.

Done and verified — merged into the integration branch via PR #210. **Verification (Docker, py3.12):** **641 backend tests pass** (+9 new in `test_statblock_schema_targeting.py`), no regressions. Migration `d8e9f0a1b2c3` up/down/up clean; `current_stats` column added. **Delivered:** schema-targeted `generate_statblock` (`generate_lore_entry_statblock_targeted` → schema-conforming `values`, validate-retry-then-fallback to #130's free-form path, sidebar/body derived from the same values for coherence); the proposed envelope staged on `lore_entry_drafts.current_stats` and written to `entry.stats` on approval (re-validated against the current schema). No-schema/generic campaigns keep #130's behaviour unchanged. **Also resolved #137's flagged follow-up** — the flesh-out backstory/statblock prompts now use `resolve_system_prompt_context`. **Follow-up flagged:** `patch_draft` doesn't yet accept `current_stats` edits during review (a GM editing a staged sidebar row won't retro-sync the structured envelope) — small follow-up if inline stat editing is wanted. Closing; ships to `main` with the v3.9.0 release.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#142
No description provided.