[GM Workbench] Generalized workbench generation endpoint + tool registry #136
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Motivation / Context
Today's generation pattern is one endpoint per tool:
POST /sessions/{session_id}/name-options/generate(webapp/backend/app/routers/sessions.py:235-264) andGET /{campaign_id}/planning/names/{category}(webapp/backend/app/routers/campaigns.py:3093-3144). That does not scale to the ~12-tool GM Workbench catalog (docs/.internal/gm-planning-expansion-2026-07-15.md§3). Name generation also carries an awkward wart worth fixing while generalizing: the custom-prompt path requires picking an active session (sessions.py:235,CampaignPlanning.jsx:545-548) purely because canonical-name selection lives onSession— an odd constraint for what is conceptually a campaign-level tool.This is issue 2 of 3 foundational items. It turns the one-off name-generation plumbing into a registry-driven surface every subsequent tool (descriptions, rumors, loot, tables, improv NPCs, prep sheets, …) registers into instead of getting its own bespoke endpoint.
Approach
webapp/backend/app/services/generation_service.pywith aGENERATOR_TOOLSregistry: a dict of tool definitions keyed bytool_id, each carrying system prompt, input schema, context builders, output schema,json_modeflag,prefetchableflag, andsync_allowedflag (per §5.1 of the report).require_gmdependency:POST /api/campaigns/{campaign_id}/workbench/{tool_id}/generate— validates params against the tool's input schema, dispatches to sync or Celery execution per the tool'ssync_allowedflag.campaigns.py:3074-3090) generalizes to any tool flaggedprefetchable, reusing the existing Redis lock + TTL pattern fromplanning_tasks.py:53-94verbatim (lock key per campaign+tool).campaigns.py:3119-3139). Prose-heavy tools (backstory, prep sheet, shop sheet) go through a Celery task modeled ongenerate_lore_entry_draft(webapp/backend/app/tasks/reminder_tasks.py:2018-2048), writing status into apending → ready/failedrecord — the same lifecycle the draft pipeline already uses, and the record this issue's status writes into is theGenerationResultmodel landing in #138 (issue 3). Rule of thumb from the report: anything that can exceed a few seconds on a local Ollama box must be async, since the hosted product cannot hold HTTP requests open on GPU-server latency._NAME_CATEGORY_DESCRIPTION_HINTS(audio_service.py:163-218),_NAME_GENERATOR_SYSTEM_PROMPT(audio_service.py:220-228), andgenerate_name_options(audio_service.py:759-826) into a registry entry. Drop the session-required constraint on the custom-prompt path — the new endpoint is campaign-scoped, so a session link becomes optional metadata rather than a gate. Saving a chosen name to aLoreEntrystill works as it does today (sessions.py:283-296).POST /sessions/{session_id}/name-options/generateandGET /{campaign_id}/planning/names/{category}continue to work, internally calling into the registry, so no frontend call sites break before #153 (Workbench UX reorganization) migrates them.campaign_name + (f" ({game_system})"),audio_service.py:780):campaign_context(campaign),lore_context(db, campaign_id, *, entry_ids | category, limit),threads_context(db, campaign_id),recent_sessions_context(db, campaign_id, n). Each tool declares which builders it needs.llm_service.pyfrom issue #134.Dependencies
llm_service) — the registry's generation calls route throughgenerate_structured_textinllm_service.py.GenerationResultmodel that issue defines; the two are typically implemented together or in tight sequence.Out of scope
GenerationResultscratchpad/history model and its endpoints — that's issue #138.GeneratorPanel) — issue #153.get_llm_config(settings_service.py:182-201) stays instance-global for now; the registry should take a resolvedLLMConfigper-request so this swap is localized later, but implementing tenant-level config is out of scope here.Acceptance criteria
POST /api/campaigns/{campaign_id}/workbench/{tool_id}/generateexists, is GM-only, validates against the tool's schema, and dispatches sync or async per the tool's flags.tool_id="names"or similar) and produces identical output to today's flow.sessions.py:235,campaigns.py:3093,campaigns.py:3074prefetch) continue to work unmodified for existing frontend callers.campaign_context,lore_context,threads_context,recent_sessions_context) exist and are used by the names tool for at leastcampaign_context.prefetchable, reusing the existing Redis lock/TTL pattern.Picking this up (final foundation item) on
feat/136-workbench-endpoint-registry→ PR ontofeat/v3.10-gm-workbench. Builds on the merged #134 (llm_service) and #138 (GenerationResult+generation_result_service). Deliversgeneration_service.pywith theGENERATOR_TOOLSregistry + context builders,POST /api/campaigns/{campaign_id}/workbench/{tool_id}/generate(GM-only, sync/Celery dispatch writing into the #138 scratchpad), name generation migrated as the first registered tool (dropping the session-required gate), old name endpoints kept as thin wrappers, and generalized prefetch. Backend only — UX reorg is #153; existing name-generation behaviour (incl. #137 system-aware hints) preserved and verified by the existing suite.Done and verified — merged into the integration branch via PR #218. All three Phase-0 foundation items (#134, #138, #136) are in.
Verification (Docker): 684 backend tests (+10
test_workbench_generation.py), ruff clean; existing name-generation tests pass unmodified (thenamestool delegates toaudio_service.generate_name_options, preserving output + the #137 hints + every test seam).Delivered:
generation_service.py— theGENERATOR_TOOLSregistry +ToolDef(adding a tool = one entry),run_toolgeneric executor, and the four context builders;POST …/workbench/{tool_id}/generatewith sync/Celery dispatch writing into #138's scratchpad; names migrated (session no longer required for custom prompts); old endpoints kept as wrappers.Deferred (tracked → #155): full prefetch generalization —
prefetch_name_optionsstays names-specific withprefetchable=Trueas the hook; the generalized worker lands with #155 (the second prefetchable tool), where it can be designed against two consumers.Closing; ships to
mainwith the v3.10.0 release.