Co-authored-by: Claude (Opus 4.6) <claude@hermes.local> Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
This commit was merged in pull request #1317.
This commit is contained in:
149
src/infrastructure/world/adapters/threejs.py
Normal file
149
src/infrastructure/world/adapters/threejs.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""Three.js world adapter — bridges Kimi's AI World Builder to WorldInterface.
|
||||
|
||||
Studied from Kimisworld.zip (issue #870). Kimi's world is a React +
|
||||
Three.js app ("AI World Builder v1.0") that exposes a JSON state API and
|
||||
accepts ``addObject`` / ``updateObject`` / ``removeObject`` commands.
|
||||
|
||||
This adapter is a stub: ``connect()`` and the core methods outline the
|
||||
HTTP / WebSocket wiring that would be needed to talk to a running instance.
|
||||
The ``observe()`` response maps Kimi's ``WorldObject`` schema to
|
||||
``PerceptionOutput`` entities so that any WorldInterface consumer can
|
||||
treat the Three.js canvas like any other game world.
|
||||
|
||||
Usage::
|
||||
|
||||
registry.register("threejs", ThreeJSWorldAdapter)
|
||||
adapter = registry.get("threejs", base_url="http://localhost:5173")
|
||||
adapter.connect()
|
||||
perception = adapter.observe()
|
||||
adapter.act(CommandInput(action="add_object", parameters={"geometry": "sphere", ...}))
|
||||
adapter.speak("Hello from Timmy", target="broadcast")
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from infrastructure.world.interface import WorldInterface
|
||||
from infrastructure.world.types import ActionResult, ActionStatus, CommandInput, PerceptionOutput
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kimi's WorldObject geometry / material vocabulary (from WorldObjects.tsx)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_VALID_GEOMETRIES = {"box", "sphere", "cylinder", "torus", "cone", "dodecahedron"}
|
||||
_VALID_MATERIALS = {"standard", "wireframe", "glass", "glow"}
|
||||
_VALID_TYPES = {"mesh", "light", "particle", "custom"}
|
||||
|
||||
|
||||
def _object_to_entity_description(obj: dict) -> str:
|
||||
"""Render a Kimi WorldObject dict as a human-readable entity string.
|
||||
|
||||
Example output: ``sphere/glow #ff006e at (2.1, 3.0, -1.5)``
|
||||
"""
|
||||
geometry = obj.get("geometry", "unknown")
|
||||
material = obj.get("material", "unknown")
|
||||
color = obj.get("color", "#ffffff")
|
||||
pos = obj.get("position", [0, 0, 0])
|
||||
obj_type = obj.get("type", "mesh")
|
||||
pos_str = "({:.1f}, {:.1f}, {:.1f})".format(*pos)
|
||||
return f"{obj_type}/{geometry}/{material} {color} at {pos_str}"
|
||||
|
||||
|
||||
class ThreeJSWorldAdapter(WorldInterface):
|
||||
"""Adapter for Kimi's Three.js AI World Builder.
|
||||
|
||||
Connects to a running Three.js world that exposes:
|
||||
- ``GET /api/world/state`` — returns current WorldObject list
|
||||
- ``POST /api/world/execute`` — accepts addObject / updateObject code
|
||||
- WebSocket ``/ws/world`` — streams state change events
|
||||
|
||||
All core methods raise ``NotImplementedError`` until HTTP wiring is
|
||||
added. Implement ``connect()`` first — it should verify that the
|
||||
Three.js app is running and optionally open a WebSocket for live events.
|
||||
|
||||
Key insight from studying Kimi's world (issue #870):
|
||||
- Objects carry a geometry, material, color, position, rotation, scale,
|
||||
and an optional *animation* string executed via ``new Function()``
|
||||
each animation frame.
|
||||
- The AI agent (``AIAgent.tsx``) moves through the world with lerp()
|
||||
targeting, cycles through moods, and pulses its core during "thinking"
|
||||
states — a model for how Timmy could manifest presence in a 3D world.
|
||||
- World complexity is tracked as a simple counter (one unit per object)
|
||||
which the AI uses to decide whether to create, modify, or upgrade.
|
||||
"""
|
||||
|
||||
def __init__(self, *, base_url: str = "http://localhost:5173") -> None:
|
||||
self._base_url = base_url.rstrip("/")
|
||||
self._connected = False
|
||||
|
||||
# -- lifecycle ---------------------------------------------------------
|
||||
|
||||
def connect(self) -> None:
|
||||
raise NotImplementedError(
|
||||
"ThreeJSWorldAdapter.connect() — verify Three.js app is running at "
|
||||
f"{self._base_url} and optionally open a WebSocket to /ws/world"
|
||||
)
|
||||
|
||||
def disconnect(self) -> None:
|
||||
self._connected = False
|
||||
logger.info("ThreeJSWorldAdapter disconnected")
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return self._connected
|
||||
|
||||
# -- core contract (stubs) ---------------------------------------------
|
||||
|
||||
def observe(self) -> PerceptionOutput:
|
||||
"""Return current Three.js world state as structured perception.
|
||||
|
||||
Expected HTTP call::
|
||||
|
||||
GET {base_url}/api/world/state
|
||||
→ {"objects": [...WorldObject], "worldComplexity": int, ...}
|
||||
|
||||
Each WorldObject becomes an entity description string.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"ThreeJSWorldAdapter.observe() — GET /api/world/state, "
|
||||
"map each WorldObject via _object_to_entity_description()"
|
||||
)
|
||||
|
||||
def act(self, command: CommandInput) -> ActionResult:
|
||||
"""Dispatch a command to the Three.js world.
|
||||
|
||||
Supported actions (mirrors Kimi's CodeExecutor API):
|
||||
- ``add_object`` — parameters: WorldObject fields (geometry, material, …)
|
||||
- ``update_object`` — parameters: id + partial WorldObject fields
|
||||
- ``remove_object`` — parameters: id
|
||||
- ``clear_world`` — parameters: (none)
|
||||
|
||||
Expected HTTP call::
|
||||
|
||||
POST {base_url}/api/world/execute
|
||||
Content-Type: application/json
|
||||
{"action": "add_object", "parameters": {...}}
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"ThreeJSWorldAdapter.act({command.action!r}) — "
|
||||
"POST /api/world/execute with serialised CommandInput"
|
||||
)
|
||||
|
||||
def speak(self, message: str, target: str | None = None) -> None:
|
||||
"""Inject a text message into the Three.js world.
|
||||
|
||||
Kimi's world does not have a native chat layer, so the recommended
|
||||
implementation is to create a short-lived ``Text`` entity at a
|
||||
visible position (or broadcast via the world WebSocket).
|
||||
|
||||
Expected WebSocket frame::
|
||||
|
||||
{"type": "timmy_speech", "text": message, "target": target}
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"ThreeJSWorldAdapter.speak() — send timmy_speech frame over "
|
||||
"/ws/world WebSocket, or POST a temporary Text entity"
|
||||
)
|
||||
Reference in New Issue
Block a user