Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 175 additions & 23 deletions src/envs/textarena_env/README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,198 @@
---
title: TextArena Environment Server
emoji: 🎮
colorFrom: yellow
colorTo: indigo
sdk: docker
pinned: false
app_port: 8000
base_path: /web
tags:
- openenv
---

# TextArena Environment

Generic wrapper for any [TextArena](https://www.textarena.ai/docs/overview) game inside OpenEnv. This module exposes the TextArena `Env` interface through the standard HTTP server/client APIs used by other OpenEnv environments, enabling quick experimentation with the full suite of word, reasoning, and multi-agent games.
A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.

## Quick Start

The simplest way to use the TextArena environment is through the `TextArenaEnv` class:

```python
from textarena import TextArenaAction, TextArenaEnv

try:
# Create environment from Docker image
textarenaenv = TextArenaEnv.from_docker_image("textarena-env:latest")

# Reset
result = textArenaEnv.reset()
print(f"Reset: {result.observation.echoed_message}")

# Send multiple messages
messages = ["Hello, World!", "Testing echo", "Final message"]

for msg in messages:
result = textArenaEnv.step(TextArenaAction(message=msg))
print(f"Sent: '{msg}'")
print(f" → Echoed: '{result.observation.echoed_message}'")
print(f" → Length: {result.observation.message_length}")
print(f" → Reward: {result.reward}")

finally:
# Always clean up
textArenaEnv.close()
```

That's it! The `TextArenaEnv.from_docker_image()` method handles:
- Starting the Docker container
- Waiting for the server to be ready
- Connecting to the environment
- Container cleanup when you call `close()`

## Features
- Works with any registered TextArena game (e.g. `Wordle-v0`, `GuessTheNumber-v0`, `Chess-v0`, ...).
- Transparent access to TextArena message streams, rewards, and state snapshots.
- Docker image for easy deployment with Python 3.11 and preinstalled dependencies.
- Example client demonstrating end-to-end interaction.
## Building the Docker Image

## Docker
Before using the environment, you need to build the Docker image:

Build the container from the project root:
```bash
# From project root
docker build -t textarena-env:latest -f server/Dockerfile .
```

## Deploying to Hugging Face Spaces

You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:

```bash
docker build -f src/envs/textarena_env/server/Dockerfile -t textarena-env:latest .
# From the environment directory (where openenv.yaml is located)
openenv push

# Or specify options
openenv push --namespace my-org --private
```

Run it with your desired game (default is `Wordle-v0`). Environment configuration is handled via env vars:
The `openenv push` command will:
1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
2. Prepare a custom build for Hugging Face Docker space (enables web interface)
3. Upload to Hugging Face (ensuring you're logged in)

### Prerequisites

- Authenticate with Hugging Face: The command will prompt for login if not already authenticated

### Options

- `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
- `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
- `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
- `--private`: Deploy the space as private (default: public)

### Examples

```bash
docker run -p 8000:8000 \
-e TEXTARENA_ENV_ID=GuessTheNumber-v0 \
-e TEXTARENA_NUM_PLAYERS=1 \
textarena-env:latest
# Push to your personal namespace (defaults to username/env-name from openenv.yaml)
openenv push

# Push to a specific repository
openenv push --repo-id my-org/my-env

# Push with a custom base image
openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest

# Push as a private space
openenv push --private

# Combine options
openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
```

After deployment, your space will be available at:
`https://huggingface.co/spaces/<repo-id>`

The deployed space includes:
- **Web Interface** at `/web` - Interactive UI for exploring the environment
- **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
- **Health Check** at `/health` - Container health monitoring

## Environment Details

### Action
**TextArenaAction**: Contains a single field
- `message` (str) - The message to echo back

### Observation
**TextArenaObservation**: Contains the echo response and metadata
- `echoed_message` (str) - The message echoed back
- `message_length` (int) - Length of the message
- `reward` (float) - Reward based on message length (length × 0.1)
- `done` (bool) - Always False for echo environment
- `metadata` (dict) - Additional info like step count

### Reward
The reward is calculated as: `message_length × 0.1`
- "Hi" → reward: 0.2
- "Hello, World!" → reward: 1.3
- Empty message → reward: 0.0

## Advanced Usage

### Connecting to an Existing Server

If you already have a TextArena environment server running, you can connect directly:

```python
from textarena import TextArenaEnv

# Connect to existing server
textarenaenv = TextArenaEnv(base_url="<ENV_HTTP_URL_HERE>")

# Use as normal
result = textarenaenv.reset()
result = textarenaenv.step(TextArenaAction(message="Hello!"))
```

Additional environment arguments can be passed using the `TEXTARENA_KW_` prefix. For example, to enable `hardcore=True`:
Note: When connecting to an existing server, `textarenaenv.close()` will NOT stop the server.

## Development & Testing

### Direct Environment Testing

Test the environment logic directly without starting the HTTP server:

```bash
docker run -p 8000:8000 \
-e TEXTARENA_ENV_ID=Wordle-v0 \
-e TEXTARENA_KW_hardcore=true \
textarena-env:latest
# From the server directory
python3 server/textarena_environment.py
```

## Python Example
This verifies that:
- Environment resets correctly
- Step executes actions properly
- State tracking works
- Rewards are calculated correctly

### Running Locally

The repository ships with a simple client script that connects to a running server (local or Docker) and plays a few turns. Run it from the repo root:
Run the server locally for development:

```bash
python examples/textarena_simple.py
uvicorn server.app:app --reload
```

The script uses `TextArenaEnv.from_docker_image` to automatically build/run the container if needed. Review the source (`examples/textarena_simple.py`) for more details and to customize the gameplay loop.
## Project Structure

```
textarena/
├── __init__.py # Module exports
├── README.md # This file
├── openenv.yaml # OpenEnv manifest
├── pyproject.toml # Project metadata and dependencies
├── uv.lock # Locked dependencies (generated)
├── client.py # TextArenaEnv client implementation
├── models.py # Action and Observation models
└── server/
├── __init__.py # Server module exports
├── textarena_environment.py # Core environment logic
├── app.py # FastAPI application
└── Dockerfile # Container image definition
```
20 changes: 3 additions & 17 deletions src/envs/textarena_env/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,9 @@
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""TextArena environment integration for OpenEnv."""
"""TextArena Environment - A simple test environment for HTTP server."""

from .client import TextArenaEnv
from .models import (
TextArenaAction,
TextArenaMessage,
TextArenaObservation,
TextArenaState,
)
from .rewards import RewardProvider, build_reward_providers
from .models import TextArenaAction, TextArenaObservation

__all__ = [
"TextArenaEnv",
"TextArenaAction",
"TextArenaObservation",
"TextArenaState",
"TextArenaMessage",
"RewardProvider",
"build_reward_providers",
]
__all__ = ["TextArenaAction", "TextArenaObservation", "TextArenaEnv"]
83 changes: 66 additions & 17 deletions src/envs/textarena_env/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,76 @@
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""HTTP client for the generic TextArena environment."""
"""
TextArena Environment HTTP Client.

from __future__ import annotations
This module provides the client for connecting to a TextArena Environment server
over HTTP.
"""

from typing import Any, Dict, TYPE_CHECKING
from typing import Dict

from core.client_types import StepResult
from core.http_env_client import HTTPEnvClient
from openenv_core.client_types import StepResult
from openenv_core.env_server.types import State
from openenv_core.http_env_client import HTTPEnvClient

from .models import (
from models import (
TextArenaAction,
TextArenaMessage,
TextArenaObservation,
TextArenaState,
)

if TYPE_CHECKING:
from core.containers.runtime import ContainerProvider


class TextArenaEnv(HTTPEnvClient[TextArenaAction, TextArenaObservation]):
"""HTTP client for the TextArena environment server."""
"""
HTTP client for the TextArena Environment.

This client connects to a TextArenaEnvironment HTTP server and provides
methods to interact with it: reset(), step(), and state access.

Example:
>>> # Connect to a running server
>>> client = TextArenaEnv(base_url="http://localhost:8000")
>>> result = client.reset()
>>> print(result.observation.echoed_message)
>>>
>>> # Send a message
>>> result = client.step(TextArenaAction(message="Hello!"))
>>> print(result.observation.echoed_message)
>>> print(result.reward)

Example with Docker:
>>> # Automatically start container and connect
>>> client = TextArenaEnv.from_docker_image("textarena-env:latest")
>>> result = client.reset()
>>> result = client.step(TextArenaAction(message="Test"))
"""

def _step_payload(self, action: TextArenaAction) -> Dict:
"""
Convert TextArenaAction to JSON payload for step request.

Args:
action: TextArenaAction instance

def _step_payload(self, action: TextArenaAction) -> Dict[str, Any]:
return {"message": action.message}
Returns:
Dictionary representation suitable for JSON encoding
"""
return {
"message": action.message,
}

def _parse_result(
self, payload: Dict[str, Any]
) -> StepResult[TextArenaObservation]:
def _parse_result(self, payload: Dict) -> StepResult[TextArenaObservation]:
"""
Parse server response into StepResult[TextArenaObservation].

Args:
payload: JSON response from server

Returns:
StepResult with TextArenaObservation
"""
obs_data = payload.get("observation", {})
messages_payload = obs_data.get("messages", [])
messages = [
Expand Down Expand Up @@ -61,7 +102,16 @@ def _parse_result(
done=payload.get("done", False),
)

def _parse_state(self, payload: Dict[str, Any]) -> TextArenaState:
def _parse_state(self, payload: Dict) -> State:
"""
Parse server response into State object.

Args:
payload: JSON response from /state endpoint

Returns:
State object with episode_id and step_count
"""
return TextArenaState(
episode_id=payload.get("episode_id"),
step_count=payload.get("step_count", 0),
Expand All @@ -73,4 +123,3 @@ def _parse_state(self, payload: Dict[str, Any]) -> TextArenaState:
last_info=payload.get("last_info", {}),
raw_state=payload.get("raw_state", {}),
)

9 changes: 6 additions & 3 deletions src/envs/textarena_env/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Common data models for the TextArena environment wrapper."""
"""
Data models for the TextArena Environment.

The textarena environment is a simple test environment that echoes back messages.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

from core.env_server.types import Action, Observation, State
from openenv_core.env_server.types import Action, Observation, State


@dataclass
Expand Down Expand Up @@ -52,4 +56,3 @@ class TextArenaState(State):
last_reward: float = 0.0
last_info: Dict[str, Any] = field(default_factory=dict)
raw_state: Dict[str, Any] = field(default_factory=dict)

7 changes: 7 additions & 0 deletions src/envs/textarena_env/openenv.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
spec_version: 1
name: textarena
type: space
runtime: fastapi
app: server.app:app
port: 8000

Loading