Source code for tools.create_character

"""S.N.E.S. Character Creation -- LLM tool wrapper.

Exposes the game_characters.create_character() function as a
callable tool so the LLM can actually persist player characters
from within the tool-call pipeline.

# 🎭💀 POSSESSED AVATAR FORGE
"""

from __future__ import annotations

import jsonutil as json
import logging
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from tool_context import ToolContext

logger = logging.getLogger(__name__)


TOOL_NAME = "create_character"
TOOL_DESCRIPTION = (
    "Create and save a new player character for the S.N.E.S. game engine. "
    "Persists the character to Redis so it survives reboots and can be "
    "loaded into any game session. The character becomes the player's "
    "active avatar immediately. Requires name and description at minimum."
)
TOOL_PARAMETERS = {
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": ("Character name. This is the avatar's identity in-game."),
        },
        "description": {
            "type": "string",
            "description": (
                "Character description: appearance, personality, backstory, "
                "class/role, and any other traits. Be detailed."
            ),
        },
        "image_url": {
            "type": "string",
            "description": (
                "Optional URL to a character reference image. If the player "
                "uploaded an image, pass that URL here."
            ),
        },
    },
    "required": ["name", "description"],
}


[docs] async def run( name: str, description: str, image_url: str = "", ctx: ToolContext | None = None, ) -> str: """Create a character and persist it to Redis. # 🎭🔥 Args: name: Character name. description: Appearance, personality, backstory. image_url: Optional reference image URL. ctx: Tool execution context. Returns: JSON result with character ID and status. """ if ctx is None: return json.dumps({"error": "No tool context available."}) user_id = getattr(ctx, "user_id", None) if not user_id: return json.dumps({"error": "No user ID in context."}) redis = getattr(ctx, "redis", None) if redis is None: return json.dumps( { "error": "No Redis connection. Character cannot be saved.", } ) try: from game_characters import create_character # Download image data if URL provided # 💀 image_data: bytes | None = None if image_url: try: import httpx from tools._safe_http import safe_http_request, safe_httpx_client # SSRF-guarded: validates redirect hops + pins the connect IP so # a model-supplied image_url cannot reach an internal host. async with safe_httpx_client(timeout=httpx.Timeout(15.0)) as client: resp = await safe_http_request( client, "GET", image_url, max_redirects=5 ) if resp.status_code == 200: image_data = resp.content logger.info( "Downloaded character image: %d bytes from %s", len(image_data), image_url[:80], ) except Exception as exc: logger.warning("Failed to download character image: %s", exc) # Continue without image data -- URL is still saved result = await create_character( user_id=str(user_id), name=name, description=description, redis=redis, image_data=image_data, image_url=image_url, ) if result.get("success"): logger.info( "Character created: '%s' (%s) for user %s", name, result.get("char_id"), user_id, ) _pub_url = result.get("public_url", "") return json.dumps( { "success": True, "char_id": result["char_id"], "name": name, "description": description[:200], "image_saved": bool(image_data), "public_url": _pub_url, "instruction": ( f"Character '{name}' has been created and set as the " f"player's active avatar. They are now ready to play. " f"Use this character as their in-game identity. " + ( f"Character image is at {_pub_url} -- " "ALWAYS use this URL for multi-file image-to-image " "when generating game art. " if _pub_url else "" ) + "Acknowledge the character creation in-universe." ), } ) else: return json.dumps( { "success": False, "error": result.get("error", "Unknown error"), } ) except ImportError: return json.dumps( { "error": "game_characters module not available.", } ) except Exception as exc: logger.exception("Character creation failed: %s", exc) return json.dumps( { "error": f"Character creation failed: {type(exc).__name__}: {str(exc)[:200]}", } )