Skip to content

Commit d2e074e

Browse files
authored
Merge pull request #181 from Lawhy/search_env
[New Env] Add web search env with Google Search (via Serper api)
2 parents 8693974 + 6fcf6f2 commit d2e074e

File tree

15 files changed

+2039
-0
lines changed

15 files changed

+2039
-0
lines changed

docs/environments.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,25 @@ The OpenEnv community has built a catalog of ready-to-run environments that cove
203203
</div>
204204
</div>
205205

206+
<div class="environment-card">
207+
<div class="environment-card__body">
208+
<span class="environment-card__tag">Web Search</span>
209+
<p class="environment-card__description">
210+
Web search environment for RL research with configurable grids, partial observability, and customizable rewards.
211+
</p>
212+
</div>
213+
<div class="environment-card__links">
214+
<a class="environment-card__icon" href="/OpenEnv/environments/websearch/" aria-label="Web Search docs">
215+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
216+
<path d="M6 3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V9l-6-6H6zm8 1.5L18.5 9H14V4.5z" fill="currentColor"/>
217+
</svg>
218+
</a>
219+
<a class="environment-card__icon environment-card__icon--hf" href="https://huggingface.co/spaces/lawhy/web_search" target="_blank" rel="noreferrer noopener" aria-label="Web Search on Hugging Face">
220+
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="" aria-hidden="true" />
221+
</a>
222+
</div>
223+
</div>
224+
206225
</div>
207226

208227
> Want to publish your own environment? Head over to the [Build Your Own Environment](environment-builder.md) guide for a step-by-step walkthrough.

docs/environments/websearch.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
--8<-- "../../src/envs/websearch_env/README.md"
2+

src/envs/websearch_env/README.md

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
---
2+
title: Web Search Environment Server
3+
emoji: 📡
4+
colorFrom: red
5+
colorTo: pink
6+
sdk: docker
7+
pinned: false
8+
app_port: 8000
9+
base_path: /web
10+
tags:
11+
- openenv
12+
---
13+
14+
# Web Search Environment
15+
16+
A web search environment that searches the web with Google Search API (via Serper.dev).
17+
18+
## Prerequisites
19+
20+
### API Key Setup
21+
22+
This environment requires a Serper.dev API key to function.
23+
24+
1. **Get your API Key:**
25+
- Visit [Serper.dev](https://serper.dev/) and sign up for an account
26+
- Navigate to your dashboard to get your API key
27+
- Free tier includes 2,500 free searches
28+
29+
2. **Configure the API Key:**
30+
31+
**For Local Development:**
32+
```bash
33+
export SERPER_API_KEY="your-api-key-here"
34+
```
35+
36+
**For Docker:**
37+
```bash
38+
docker run -e SERPER_API_KEY="your-api-key-here" web_search-env:latest
39+
```
40+
41+
**For Hugging Face Spaces (after deployment):**
42+
- Navigate to your Space's settings page: `https://huggingface.co/spaces/USERNAME/SPACE_NAME/settings`
43+
- Scroll to the "Repository secrets" section
44+
- Click "New secret"
45+
- Name: `SERPER_API_KEY`
46+
- Value: Your Serper.dev API key
47+
- Click "Add"
48+
- The Space will automatically restart and use your API key
49+
50+
> **Important:** Never commit your API key to code. Always use environment variables or secrets management.
51+
52+
## Quick Start
53+
54+
The simplest way to use the Web Search environment is through the `WebSearchEnvironment` class:
55+
56+
```python
57+
from envs.websearch_env.server.websearch_env_environment import WebSearchEnvironment
58+
from envs.websearch_env import WebSearchAction
59+
60+
try:
61+
# Create environment from Docker image
62+
web_search_env = WebSearchEnvironment.from_docker_image("web_search-env:latest")
63+
64+
# Reset
65+
result = web_search_env.reset()
66+
print(f"Reset: {result.observation.content}")
67+
68+
# Send a search query
69+
query = "What is the capital of China?"
70+
71+
result = web_search_env.step(WebSearchAction(query=query))
72+
print(f"Formatted search result:", result.observation.content)
73+
print(f"Individual web contents:", result.observation.web_contents)
74+
75+
finally:
76+
# Always clean up
77+
web_search_env.close()
78+
```
79+
80+
That's it! The `WebSearchEnvironment.from_docker_image()` method handles:
81+
- Starting the Docker container
82+
- Waiting for the server to be ready
83+
- Connecting to the environment
84+
- Container cleanup when you call `close()`
85+
86+
## Building the Docker Image
87+
88+
Before using the environment, you need to build the Docker image:
89+
90+
```bash
91+
# From project root
92+
docker build -t web_search-env:latest -f server/Dockerfile .
93+
```
94+
95+
## Deploying to Hugging Face Spaces
96+
97+
You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
98+
99+
```bash
100+
# From the environment directory (where openenv.yaml is located)
101+
openenv push
102+
103+
# Or specify options
104+
openenv push --namespace my-org --private
105+
```
106+
107+
The `openenv push` command will:
108+
1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
109+
2. Prepare a custom build for Hugging Face Docker space (enables web interface)
110+
3. Upload to Hugging Face (ensuring you're logged in)
111+
112+
### Prerequisites
113+
114+
- Authenticate with Hugging Face: The command will prompt for login if not already authenticated
115+
116+
### Options
117+
118+
- `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
119+
- `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
120+
- `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
121+
- `--private`: Deploy the space as private (default: public)
122+
123+
### Examples
124+
125+
```bash
126+
# Push to your personal namespace (defaults to username/env-name from openenv.yaml)
127+
openenv push
128+
129+
# Push to a specific repository
130+
openenv push --repo-id my-org/my-env
131+
132+
# Push with a custom base image
133+
openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
134+
135+
# Push as a private space
136+
openenv push --private
137+
138+
# Combine options
139+
openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
140+
```
141+
142+
After deployment, your space will be available at:
143+
`https://huggingface.co/spaces/<repo-id>`
144+
145+
**⚠️ Important: Configure your API key!**
146+
After deployment, you must add your Serper.dev API key as a secret in the Space settings (see [API Key Setup](#api-key-setup) above). The environment will not work without it.
147+
148+
The deployed space includes:
149+
- **Web Interface** at `/web` - Interactive UI for exploring the environment
150+
- **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
151+
- **Health Check** at `/health` - Container health monitoring
152+
153+
## Environment Details
154+
155+
### Action
156+
**WebSearchAction**: Contains a single field
157+
- `query` (str) - The query to search for
158+
- `temp_api_key` (str) - Temporary Serper.dev API key if not set in envrionment variables.
159+
160+
### Observation
161+
**WebSearchObservation**: Contains the echo response and metadata
162+
- `content` (str) - The formatted prompt that aggregates both query and web contents
163+
- `web_contents` (list) - List of web contents for top ranked web pages
164+
- `reward` (float) - Reward is not defined in this scenario
165+
- `done` (bool) - Always False for search environment
166+
- `metadata` (dict) - Additional info like step count
167+
168+
### Reward
169+
The reward is undefined here.
170+
171+
## Advanced Usage
172+
173+
### Connecting to an Existing Server
174+
175+
If you already have a Web Search environment server running, you can connect directly:
176+
177+
```python
178+
from envs.websearch_env import WebSearchEnvironment
179+
180+
# Connect to existing server
181+
web_search_env = WebSearchEnvironment(base_url="<ENV_HTTP_URL_HERE>")
182+
183+
# Use as normal
184+
result = web_search_env.reset()
185+
result = web_search_env.step(WebSearchAction(query="What is the capital of China?"))
186+
```
187+
188+
Note: When connecting to an existing server, `web_search_env.close()` will NOT stop the server.
189+
190+
## Development & Testing
191+
192+
### Direct Environment Testing
193+
194+
Test the environment logic directly without starting the HTTP server:
195+
196+
```bash
197+
# From the server directory
198+
python3 server/web_search_environment.py
199+
```
200+
201+
This verifies that:
202+
- Environment resets correctly
203+
- Step executes actions properly
204+
- State tracking works
205+
- Rewards are calculated correctly
206+
207+
### Running Locally
208+
209+
Run the server locally for development:
210+
211+
```bash
212+
# Make sure to set your API key first
213+
export SERPER_API_KEY="your-api-key-here"
214+
215+
# Then run the server
216+
uvicorn server.app:app --reload
217+
```
218+
219+
## Project Structure
220+
221+
```
222+
web_search/
223+
├── __init__.py # Module exports
224+
├── README.md # This file
225+
├── openenv.yaml # OpenEnv manifest
226+
├── pyproject.toml # Project metadata and dependencies
227+
├── uv.lock # Locked dependencies (generated)
228+
├── client.py # WebSearchEnv client implementation
229+
├── models.py # Action and Observation models
230+
└── server/
231+
├── __init__.py # Server module exports
232+
├── websearch_env_environment.py # Core environment logic
233+
├── app.py # FastAPI application
234+
└── Dockerfile # Container image definition
235+
```

src/envs/websearch_env/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""WebSearch Env Environment - A web search environment that uses Google Search API (via Serper.dev)."""
8+
9+
from .client import WebSearchEnv
10+
from .models import WebSearchAction, WebSearchObservation
11+
12+
__all__ = ["WebSearchAction", "WebSearchObservation", "WebSearchEnv"]

src/envs/websearch_env/client.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""
8+
WebSearch Env Environment HTTP Client.
9+
10+
This module provides the client for connecting to a WebSearch Env Environment server
11+
over HTTP.
12+
"""
13+
14+
from typing import Dict
15+
16+
from openenv_core.client_types import StepResult
17+
from openenv_core.env_server.types import State
18+
from openenv_core.http_env_client import HTTPEnvClient
19+
20+
from .models import WebSearchAction, WebSearchObservation
21+
22+
23+
class WebSearchEnv(HTTPEnvClient[WebSearchAction, WebSearchObservation]):
24+
"""
25+
HTTP client for the WebSearch Env Environment.
26+
27+
This client connects to a WebSearchEnvironment HTTP server and provides
28+
methods to interact with it: reset(), step(), and state access.
29+
30+
Example:
31+
>>> # Connect to a running server
32+
>>> client = WebSearchEnv(base_url="http://localhost:8000")
33+
>>> result = client.reset()
34+
>>> print(result.observation.echoed_message)
35+
>>>
36+
>>> # Send a message
37+
>>> result = client.step(WebSearchAction(message="Hello!"))
38+
>>> print(result.observation.echoed_message)
39+
>>> print(result.reward)
40+
41+
Example with Docker:
42+
>>> # Automatically start container and connect
43+
>>> client = WebSearchEnv.from_docker_image("WebSearch_env-env:latest")
44+
>>> result = client.reset()
45+
>>> result = client.step(WebSearchAction(message="Test"))
46+
"""
47+
48+
def _step_payload(self, action: WebSearchAction) -> Dict:
49+
"""
50+
Convert WebSearchAction to JSON payload for step request.
51+
52+
Args:
53+
action: WebSearchAction instance
54+
55+
Returns:
56+
Dictionary representation suitable for JSON encoding
57+
"""
58+
return {
59+
"query": action.query,
60+
}
61+
62+
def _parse_result(self, payload: Dict) -> StepResult[WebSearchObservation]:
63+
"""
64+
Parse server response into StepResult[WebSearchObservation].
65+
66+
Args:
67+
payload: JSON response from server
68+
69+
Returns:
70+
StepResult with WebSearchObservation
71+
"""
72+
obs_data = payload.get("observation", {})
73+
observation = WebSearchObservation(
74+
content=obs_data.get("content", ""),
75+
web_contents=obs_data.get("web_contents", []),
76+
metadata=obs_data.get("metadata", {}),
77+
)
78+
79+
return StepResult(
80+
observation=observation,
81+
reward=payload.get("reward"),
82+
done=payload.get("done", False),
83+
)
84+
85+
def _parse_state(self, payload: Dict) -> State:
86+
"""
87+
Parse server response into State object.
88+
89+
Args:
90+
payload: JSON response from /state endpoint
91+
92+
Returns:
93+
State object with episode_id and step_count
94+
"""
95+
return State(
96+
episode_id=payload.get("episode_id"),
97+
step_count=payload.get("step_count", 0),
98+
)

0 commit comments

Comments
 (0)