import uuid from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.character import Character from app.models.game import GameParticipant from app.models.user import User TOOL_SCHEMA = { "name": "link_existing_character", "description": ( "Call this when a player wants to bring a character they already created in a previous " "game, identified by name — instead of building a new one. Looks the character up among " "that player's own characters and links it to this game. Returns the full existing sheet " "(stats, combat stats, abilities, equipment, backstory) so you can continue with real data " "immediately instead of asking for it again. Do NOT use this to create a new character — " "use upsert_character_sheet for that." ), "parameters": { "type": "object", "properties": { "player_name": { "type": "string", "description": "The real first name of the player this character belongs to, exactly as they introduced themselves.", }, "character_name": { "type": "string", "description": "The character's name, as the player stated it.", }, }, "required": ["player_name", "character_name"], }, } async def link(session: AsyncSession, game_id: uuid.UUID, tool_input: dict) -> dict: player_name = tool_input["player_name"] character_name = tool_input["character_name"] participant_row = ( await session.execute( select(GameParticipant, User) .join(User, User.id == GameParticipant.user_id) .where(GameParticipant.game_id == game_id, User.name == player_name) ) ).first() if participant_row is None: raise ValueError(f"No participant named {player_name!r} found in this game") participant, user = participant_row # Only ever search the requesting player's own characters — never let the DM link a # character belonging to someone else, even if the name happens to match. candidates = ( await session.execute(select(Character).where(Character.owner_id == user.id)) ).scalars().all() match = next( (c for c in candidates if c.name.strip().lower() == character_name.strip().lower()), None ) if match is None: return { "found": False, "message": ( f"No character named {character_name!r} found for {player_name}. Ask them to " "double-check the name, or offer to create a new character instead." ), } participant.character_id = match.id await session.commit() return { "found": True, "character_id": str(match.id), "name": match.name, "race": match.race, "char_class": match.char_class, "level": match.level, "stats": match.stats, "combat_stats": match.combat_stats, "current_hp": match.current_hp, "max_hp": match.max_hp, "abilities": match.abilities, "equipment": match.equipment, "description": match.description, }