Add rulebook RAG pipeline and LLM-driven game setup wizard
RAG: switch Postgres to pgvector, chunk and embed the three D&D rulebooks locally via sentence-transformers, and retrieve relevant excerpts per DM turn (query = latest player message) to ground the system prompt. Retrieval runs off the event loop and is capped by a relevance threshold and a max character budget so it can't blow up context size or cost. Game setup wizard: creating a game now opens a short chat where the DM asks about genre, length, and the player's experience level, then proposes a name and description via a tool call. The player can edit both before creating the game. Stateless endpoint — the frontend carries the conversation, no DB needed since the game doesn't exist yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import json
|
||||
|
||||
from app.config import settings
|
||||
from app.llm.client import get_game_setup_prompt, get_llm_client
|
||||
from app.llm.tools import game_setup as game_setup_tool
|
||||
|
||||
MAX_ROUNDS = 3
|
||||
MAX_TOKENS = 1024
|
||||
|
||||
TOOLS = [{"type": "function", "function": game_setup_tool.TOOL_SCHEMA}]
|
||||
|
||||
|
||||
async def run_game_setup_turn(client_messages: list[dict]) -> 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()
|
||||
messages = [{"role": "system", "content": get_game_setup_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 = 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"}),
|
||||
}
|
||||
)
|
||||
if proposal:
|
||||
break
|
||||
|
||||
return {"messages": messages[1:], "assistant_text": assistant_text, "proposal": proposal}
|
||||
Reference in New Issue
Block a user