Skip to content

Commit 1f4fb0a

Browse files
bugszXuhuiZhou
andauthored
Deploy the api to modal (#267)
* prototype for modal serving * add openai secret * fix type annotation * add doc * bug fix for simulation api * add customize model, evaluator model and evaluation dimensions * Implement modal API server with Redis integration and FastAPI setup - Added a new script for the modal API server that initializes a Redis instance. - Created a persistent volume for Redis data and included a function to download initial data if not present. - Configured a Docker image with necessary dependencies including Redis Stack and FastAPI. - Implemented a web API class that sets up and cleans up the Redis connection, ensuring readiness before serving requests. - Integrated the SotopiaFastAPI application within the modal framework. --------- Co-authored-by: XuhuiZhou <[email protected]>
1 parent ab6903a commit 1f4fb0a

File tree

5 files changed

+572
-411
lines changed

5 files changed

+572
-411
lines changed

docs/pages/examples/deployment.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Deploy Sotopia Python API to Modal
2+
We offer a script to deploy Sotopia Python API to [Modal](https://modal.com/).
3+
To do so, simply go to the `sotopia/sotopia/ui` directory and run the following command:
4+
```bash
5+
modal deploy sotopia/ui/modal_api_server.py
6+
```

scripts/modal/modal_api_server.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import modal
2+
import subprocess
3+
import time
4+
import os
5+
6+
import redis
7+
from sotopia.ui.fastapi_server import SotopiaFastAPI
8+
9+
# Create persistent volume for Redis data
10+
redis_volume = modal.Volume.from_name("sotopia-api", create_if_missing=True)
11+
12+
13+
def initialize_redis_data() -> None:
14+
"""Download Redis data if it doesn't exist"""
15+
if not os.path.exists("/vol/redis/dump.rdb"):
16+
os.makedirs("/vol/redis", exist_ok=True)
17+
print("Downloading initial Redis data...")
18+
subprocess.run(
19+
"curl -L https://cmu.box.com/shared/static/e3vd31r7916jb70j9cgtcq9etryrxml0.rdb --output /vol/redis/dump.rdb",
20+
shell=True,
21+
check=True,
22+
)
23+
print("Redis data downloaded")
24+
25+
26+
# Create image with all necessary dependencies
27+
image = (
28+
modal.Image.debian_slim(python_version="3.11")
29+
.apt_install(
30+
"git",
31+
"curl",
32+
"gpg",
33+
"lsb-release",
34+
"wget",
35+
"procps", # for ps command
36+
"redis-tools", # for redis-cli
37+
)
38+
.run_commands(
39+
# Update and install basic dependencies
40+
"apt-get update",
41+
"apt-get install -y curl gpg lsb-release",
42+
# Add Redis Stack repository
43+
"curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg",
44+
"chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg",
45+
'echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list',
46+
"apt-get update",
47+
"apt-get install -y redis-stack-server",
48+
)
49+
.pip_install(
50+
"sotopia>=0.1.2",
51+
"fastapi>0.100", # TODO: remove this dependency after pypi release
52+
"uvicorn", # TODO: remove this dependency after pypi release
53+
)
54+
)
55+
redis_volume = modal.Volume.from_name("sotopia-api", create_if_missing=True)
56+
57+
# Create stub for the application
58+
app = modal.App("sotopia-fastapi", image=image, volumes={"/vol/redis": redis_volume})
59+
60+
61+
@app.cls(
62+
image=image,
63+
concurrency_limit=1,
64+
allow_concurrent_inputs=5,
65+
secrets=[modal.Secret.from_name("openai-secret")],
66+
)
67+
class WebAPI:
68+
def __init__(self) -> None:
69+
self.web_app = SotopiaFastAPI()
70+
71+
@modal.enter()
72+
def setup(self) -> None:
73+
# Start Redis server
74+
subprocess.Popen(
75+
["redis-stack-server", "--dir", "/vol/redis", "--port", "6379"]
76+
)
77+
78+
# Wait for Redis to be ready
79+
max_retries = 30
80+
for _ in range(max_retries):
81+
try:
82+
initialize_redis_data()
83+
# Attempt to create Redis client and ping the server
84+
temp_client = redis.Redis(host="localhost", port=6379, db=0)
85+
temp_client.ping()
86+
self.redis_client = temp_client
87+
print("Successfully connected to Redis")
88+
return
89+
except (redis.exceptions.ConnectionError, redis.exceptions.ResponseError):
90+
print("Waiting for Redis to be ready...")
91+
time.sleep(1)
92+
93+
raise Exception("Could not connect to Redis after multiple attempts")
94+
95+
@modal.exit()
96+
def cleanup(self) -> None:
97+
if hasattr(self, "redis_client"):
98+
self.redis_client.close()
99+
100+
@modal.asgi_app()
101+
def serve(self) -> SotopiaFastAPI:
102+
return self.web_app

sotopia/ui/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ returns:
155155
**WSMessageType**
156156
| Type | Direction | Description |
157157
|-----------|--------|-------------|
158-
| SERVER_MSG | Server → Client | Standard message from server (payload: `messageForRendering` [here](https://github.com/sotopia-lab/sotopia-demo/blob/main/socialstream/rendering_utils.py) ) |
158+
| SERVER_MSG | Server → Client | Standard message from server (payload: `EpisodeLog`) |
159159
| CLIENT_MSG | Client → Server | Standard message from client (payload: TBD) |
160160
| ERROR | Server → Client | Error notification (payload: TBD) |
161161
| START_SIM | Client → Server | Initialize simulation (payload: `SimulationEpisodeInitialization`) |
@@ -179,6 +179,13 @@ returns:
179179

180180
**Implementation plan**: Currently only support LLM-LLM simulation based on [this function](https://github.com/sotopia-lab/sotopia/blob/19d39e068c3bca9246fc366e5759414f62284f93/sotopia/server.py#L108).
181181

182+
**SERVER_MSG payload**
183+
The server message is a dictionary that has the following keys:
184+
- type: str, indicates the type of the message, typically it is "messages"
185+
- messages: Any. Typically this is the dictionary of the `EpisodeLog` for the current simulation state. (Which means the reward part could be empty)
186+
187+
188+
182189

183190
## An example to run simulation with the API
184191

0 commit comments

Comments
 (0)