Files
DungeonsDragons/backend/app/llm/game_setup.py
T
Thorsten 07665595a2 Move adventure-text field to the front of game setup
The adventure-text field was buried at the bottom of the proposal
form, after the whole interview — the user wanted it to be the very
first thing shown when clicking "Neues Spiel", since pasting an
adventure should drive the proposal rather than follow it.

GameSetupChat now opens on an "Eigenes Abenteuer (optional)" step:
paste the text (or skip) before the wizard chat starts at all. The
setup-chat backend call now receives that text and, when present,
infers genre/world/scope from its opening directly instead of asking
about them — the proposed name/description come out already specific
to the pasted adventure. Removed the now-redundant adventure textarea
from the proposal form (it's collected once, upfront).

Verified live: pasting a short pirate one-shot produced "Klingt nach
einem klassischen Piraten-One-Shot rund um Port Royal – das Genre und
den Umfang leite ich direkt daraus ab", skipping straight to the one
remaining question (experience level), and the final proposal named
Isabella Steel and Blackfin's fortress directly from the text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 13:35:45 +02:00

68 lines
2.7 KiB
Python

import json
from app.config import settings
from app.llm.client import get_game_setup_prompt, get_llm_client
from app.llm.json_utils import fix_double_escaped_unicode
from app.llm.tools import game_setup as game_setup_tool
MAX_ROUNDS = 3
MAX_TOKENS = 1024
# Only the opening of a pasted adventure is needed here to infer genre/world/scope for the
# proposal — this is a cheap interview call, not the RAG pipeline that handles the full text.
ADVENTURE_EXCERPT_CHARS = 3000
TOOLS = [{"type": "function", "function": game_setup_tool.TOOL_SCHEMA}]
async def run_game_setup_turn(client_messages: list[dict], adventure_text: str = "") -> dict:
"""Stateless setup-wizard turn: the caller owns conversation history (no DB, no game yet).
Returns {"messages": <history to resend next turn>, "assistant_text": str | None,
"proposal": {"name": str, "description": str} | None}.
"""
client = get_llm_client()
system_prompt = get_game_setup_prompt()
if adventure_text.strip():
excerpt = adventure_text.strip()[:ADVENTURE_EXCERPT_CHARS]
system_prompt = (
f"{system_prompt}\n\nDer Spieler hat bereits ein vorhandenes Abenteuer mitgegeben "
f"(Anfang unten). Leite Genre/Welt daraus ab, statt danach zu fragen — frage nur noch, "
f"was daraus nicht hervorgeht:\n\n{excerpt}"
)
messages = [{"role": "system", "content": system_prompt}] + client_messages
assistant_text: str | None = None
proposal: dict | None = None
for _ in range(MAX_ROUNDS):
response = await client.chat.completions.create(
model=settings.dm_model,
max_tokens=MAX_TOKENS,
messages=messages,
tools=TOOLS,
)
message = response.choices[0].message
if message.content:
assistant_text = message.content
if not message.tool_calls:
messages.append({"role": "assistant", "content": message.content or ""})
break
messages.append(message.model_dump(exclude_unset=True))
for tool_call in message.tool_calls:
args = fix_double_escaped_unicode(json.loads(tool_call.function.arguments))
if tool_call.function.name == "propose_game_setup":
proposal = {"name": args.get("name", ""), "description": args.get("description", "")}
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps({"status": "ok"}, ensure_ascii=False),
}
)
if proposal:
break
return {"messages": messages[1:], "assistant_text": assistant_text, "proposal": proposal}