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:
Thorsten
2026-08-31 20:03:05 +02:00
parent f37dc9fa76
commit 419f5e3a89
26 changed files with 82471 additions and 51 deletions
+14 -4
View File
@@ -4,13 +4,14 @@ import uuid
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger("app.llm.orchestrator")
from app.config import settings
from app.llm.client import get_dm_system_prompt, get_llm_client
from app.llm.context import build_context
from app.llm.tools import character_sheet, dice
from app.models.message import Message
from app.rag.retrieval import build_rag_block
logger = logging.getLogger("app.llm.orchestrator")
MAX_TOOL_ROUNDS = 5
MAX_TOKENS = 4096
@@ -38,9 +39,18 @@ async def _execute_tool_call(
return {"error": str(exc)}
async def run_dm_turn(session: AsyncSession, game_id: uuid.UUID) -> Message:
async def run_dm_turn(
session: AsyncSession, game_id: uuid.UUID, latest_player_message: str | None = None
) -> Message:
client = get_llm_client()
messages = [{"role": "system", "content": get_dm_system_prompt()}]
system_prompt = get_dm_system_prompt()
if latest_player_message:
rag_block = await build_rag_block(session, latest_player_message)
if rag_block:
system_prompt = f"{system_prompt}\n\n{rag_block}"
messages = [{"role": "system", "content": system_prompt}]
messages.extend(await build_context(session, game_id))
final_text = ""