07665595a2
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>
68 lines
2.7 KiB
Python
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}
|