"""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]}",
}
)