DJAYADEV commited on
Commit
65128c5
·
verified ·
1 Parent(s): 88305cc

Upload folder using huggingface_hub

Browse files
Files changed (14) hide show
  1. Dockerfile +81 -0
  2. LICENSE +28 -0
  3. README.md +250 -5
  4. __init__.py +16 -0
  5. client.py +56 -0
  6. inference.py +159 -0
  7. models.py +62 -0
  8. openenv.yaml +7 -0
  9. pyproject.toml +45 -0
  10. server/__init__.py +11 -0
  11. server/app.py +79 -0
  12. server/rag_optimizer_environment.py +209 -0
  13. server/requirements.txt +4 -0
  14. uv.lock +0 -0
Dockerfile ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Multi-stage build using openenv-base
8
+ # This Dockerfile is flexible and works for both:
9
+ # - In-repo environments (with local OpenEnv sources)
10
+ # - Standalone environments (with openenv from PyPI/Git)
11
+ # The build script (openenv build) handles context detection and sets appropriate build args.
12
+
13
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
14
+ FROM ${BASE_IMAGE} AS builder
15
+
16
+ WORKDIR /app
17
+
18
+ # Ensure git is available (required for installing dependencies from VCS)
19
+ RUN apt-get update && \
20
+ apt-get install -y --no-install-recommends git && \
21
+ rm -rf /var/lib/apt/lists/*
22
+
23
+ # Build argument to control whether we're building standalone or in-repo
24
+ ARG BUILD_MODE=in-repo
25
+ ARG ENV_NAME=rag_optimizer
26
+
27
+ # Copy environment code (always at root of build context)
28
+ COPY . /app/env
29
+
30
+ # For in-repo builds, openenv is already vendored in the build context
31
+ # For standalone builds, openenv will be installed via pyproject.toml
32
+ WORKDIR /app/env
33
+
34
+ # Ensure uv is available (for local builds where base image lacks it)
35
+ RUN if ! command -v uv >/dev/null 2>&1; then \
36
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
37
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
38
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
39
+ fi
40
+
41
+ # Install dependencies using uv sync
42
+ # If uv.lock exists, use it; otherwise resolve on the fly
43
+ RUN --mount=type=cache,target=/root/.cache/uv \
44
+ if [ -f uv.lock ]; then \
45
+ uv sync --frozen --no-install-project --no-editable; \
46
+ else \
47
+ uv sync --no-install-project --no-editable; \
48
+ fi
49
+
50
+ RUN --mount=type=cache,target=/root/.cache/uv \
51
+ if [ -f uv.lock ]; then \
52
+ uv sync --frozen --no-editable; \
53
+ else \
54
+ uv sync --no-editable; \
55
+ fi
56
+
57
+ # Final runtime stage
58
+ FROM ${BASE_IMAGE}
59
+
60
+ WORKDIR /app
61
+
62
+ # Copy the virtual environment from builder
63
+ COPY --from=builder /app/env/.venv /app/.venv
64
+
65
+ # Copy the environment code
66
+ COPY --from=builder /app/env /app/env
67
+
68
+ # Set PATH to use the virtual environment
69
+ ENV PATH="/app/.venv/bin:$PATH"
70
+
71
+ # Set PYTHONPATH so imports work correctly
72
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
73
+
74
+ # Health check
75
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
76
+ CMD curl -f http://localhost:8000/health || exit 1
77
+
78
+ # Run the FastAPI server
79
+ # The module path is constructed to work with the /app/env structure
80
+ ENV ENABLE_WEB_INTERFACE=true
81
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
LICENSE ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Jayadev D
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
README.md CHANGED
@@ -1,10 +1,255 @@
1
  ---
2
- title: Rag Optimizer
3
- emoji: 👁
4
- colorFrom: purple
5
- colorTo: indigo
6
  sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Rag Optimizer Environment Server
3
+ emoji: 📸
4
+ colorFrom: blue
5
+ colorTo: gray
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
  ---
13
 
14
+ # Rag Optimizer Environment
15
+
16
+ A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
17
+
18
+ ## Quick Start
19
+
20
+ The simplest way to use the Rag Optimizer environment is through the `RagOptimizerEnv` class:
21
+
22
+ ```python
23
+ from rag_optimizer import RagOptimizerAction, RagOptimizerEnv
24
+
25
+ try:
26
+ # Create environment from Docker image
27
+ rag_optimizerenv = RagOptimizerEnv.from_docker_image("rag_optimizer-env:latest")
28
+
29
+ # Reset
30
+ result = rag_optimizerenv.reset()
31
+ print(f"Reset: {result.observation.echoed_message}")
32
+
33
+ # Send multiple messages
34
+ messages = ["Hello, World!", "Testing echo", "Final message"]
35
+
36
+ for msg in messages:
37
+ result = rag_optimizerenv.step(RagOptimizerAction(message=msg))
38
+ print(f"Sent: '{msg}'")
39
+ print(f" → Echoed: '{result.observation.echoed_message}'")
40
+ print(f" → Length: {result.observation.message_length}")
41
+ print(f" → Reward: {result.reward}")
42
+
43
+ finally:
44
+ # Always clean up
45
+ rag_optimizerenv.close()
46
+ ```
47
+
48
+ That's it! The `RagOptimizerEnv.from_docker_image()` method handles:
49
+ - Starting the Docker container
50
+ - Waiting for the server to be ready
51
+ - Connecting to the environment
52
+ - Container cleanup when you call `close()`
53
+
54
+ ## Building the Docker Image
55
+
56
+ Before using the environment, you need to build the Docker image:
57
+
58
+ ```bash
59
+ # From project root
60
+ docker build -t rag_optimizer-env:latest -f server/Dockerfile .
61
+ ```
62
+
63
+ ## Deploying to Hugging Face Spaces
64
+
65
+ You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
66
+
67
+ ```bash
68
+ # From the environment directory (where openenv.yaml is located)
69
+ openenv push
70
+
71
+ # Or specify options
72
+ openenv push --namespace my-org --private
73
+ ```
74
+
75
+ The `openenv push` command will:
76
+ 1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
77
+ 2. Prepare a custom build for Hugging Face Docker space (enables web interface)
78
+ 3. Upload to Hugging Face (ensuring you're logged in)
79
+
80
+ ### Prerequisites
81
+
82
+ - Authenticate with Hugging Face: The command will prompt for login if not already authenticated
83
+
84
+ ### Options
85
+
86
+ - `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
87
+ - `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
88
+ - `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
89
+ - `--private`: Deploy the space as private (default: public)
90
+
91
+ ### Examples
92
+
93
+ ```bash
94
+ # Push to your personal namespace (defaults to username/env-name from openenv.yaml)
95
+ openenv push
96
+
97
+ # Push to a specific repository
98
+ openenv push --repo-id my-org/my-env
99
+
100
+ # Push with a custom base image
101
+ openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
102
+
103
+ # Push as a private space
104
+ openenv push --private
105
+
106
+ # Combine options
107
+ openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
108
+ ```
109
+
110
+ After deployment, your space will be available at:
111
+ `https://huggingface.co/spaces/<repo-id>`
112
+
113
+ The deployed space includes:
114
+ - **Web Interface** at `/web` - Interactive UI for exploring the environment
115
+ - **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
116
+ - **Health Check** at `/health` - Container health monitoring
117
+ - **WebSocket** at `/ws` - Persistent session endpoint for low-latency interactions
118
+
119
+ ## Environment Details
120
+
121
+ ### Action
122
+ **RagOptimizerAction**: Contains a single field
123
+ - `message` (str) - The message to echo back
124
+
125
+ ### Observation
126
+ **RagOptimizerObservation**: Contains the echo response and metadata
127
+ - `echoed_message` (str) - The message echoed back
128
+ - `message_length` (int) - Length of the message
129
+ - `reward` (float) - Reward based on message length (length × 0.1)
130
+ - `done` (bool) - Always False for echo environment
131
+ - `metadata` (dict) - Additional info like step count
132
+
133
+ ### Reward
134
+ The reward is calculated as: `message_length × 0.1`
135
+ - "Hi" → reward: 0.2
136
+ - "Hello, World!" → reward: 1.3
137
+ - Empty message → reward: 0.0
138
+
139
+ ## Advanced Usage
140
+
141
+ ### Connecting to an Existing Server
142
+
143
+ If you already have a Rag Optimizer environment server running, you can connect directly:
144
+
145
+ ```python
146
+ from rag_optimizer import RagOptimizerEnv
147
+
148
+ # Connect to existing server
149
+ rag_optimizerenv = RagOptimizerEnv(base_url="<ENV_HTTP_URL_HERE>")
150
+
151
+ # Use as normal
152
+ result = rag_optimizerenv.reset()
153
+ result = rag_optimizerenv.step(RagOptimizerAction(message="Hello!"))
154
+ ```
155
+
156
+ Note: When connecting to an existing server, `rag_optimizerenv.close()` will NOT stop the server.
157
+
158
+ ### Using the Context Manager
159
+
160
+ The client supports context manager usage for automatic connection management:
161
+
162
+ ```python
163
+ from rag_optimizer import RagOptimizerAction, RagOptimizerEnv
164
+
165
+ # Connect with context manager (auto-connects and closes)
166
+ with RagOptimizerEnv(base_url="http://localhost:8000") as env:
167
+ result = env.reset()
168
+ print(f"Reset: {result.observation.echoed_message}")
169
+ # Multiple steps with low latency
170
+ for msg in ["Hello", "World", "!"]:
171
+ result = env.step(RagOptimizerAction(message=msg))
172
+ print(f"Echoed: {result.observation.echoed_message}")
173
+ ```
174
+
175
+ The client uses WebSocket connections for:
176
+ - **Lower latency**: No HTTP connection overhead per request
177
+ - **Persistent session**: Server maintains your environment state
178
+ - **Efficient for episodes**: Better for many sequential steps
179
+
180
+ ### Concurrent WebSocket Sessions
181
+
182
+ The server supports multiple concurrent WebSocket connections. To enable this,
183
+ modify `server/app.py` to use factory mode:
184
+
185
+ ```python
186
+ # In server/app.py - use factory mode for concurrent sessions
187
+ app = create_app(
188
+ RagOptimizerEnvironment, # Pass class, not instance
189
+ RagOptimizerAction,
190
+ RagOptimizerObservation,
191
+ max_concurrent_envs=4, # Allow 4 concurrent sessions
192
+ )
193
+ ```
194
+
195
+ Then multiple clients can connect simultaneously:
196
+
197
+ ```python
198
+ from rag_optimizer import RagOptimizerAction, RagOptimizerEnv
199
+ from concurrent.futures import ThreadPoolExecutor
200
+
201
+ def run_episode(client_id: int):
202
+ with RagOptimizerEnv(base_url="http://localhost:8000") as env:
203
+ result = env.reset()
204
+ for i in range(10):
205
+ result = env.step(RagOptimizerAction(message=f"Client {client_id}, step {i}"))
206
+ return client_id, result.observation.message_length
207
+
208
+ # Run 4 episodes concurrently
209
+ with ThreadPoolExecutor(max_workers=4) as executor:
210
+ results = list(executor.map(run_episode, range(4)))
211
+ ```
212
+
213
+ ## Development & Testing
214
+
215
+ ### Direct Environment Testing
216
+
217
+ Test the environment logic directly without starting the HTTP server:
218
+
219
+ ```bash
220
+ # From the server directory
221
+ python3 server/rag_optimizer_environment.py
222
+ ```
223
+
224
+ This verifies that:
225
+ - Environment resets correctly
226
+ - Step executes actions properly
227
+ - State tracking works
228
+ - Rewards are calculated correctly
229
+
230
+ ### Running Locally
231
+
232
+ Run the server locally for development:
233
+
234
+ ```bash
235
+ uvicorn server.app:app --reload
236
+ ```
237
+
238
+ ## Project Structure
239
+
240
+ ```
241
+ rag_optimizer/
242
+ ├── .dockerignore # Docker build exclusions
243
+ ├── __init__.py # Module exports
244
+ ├── README.md # This file
245
+ ├── openenv.yaml # OpenEnv manifest
246
+ ├── pyproject.toml # Project metadata and dependencies
247
+ ├── uv.lock # Locked dependencies (generated)
248
+ ├── client.py # RagOptimizerEnv client
249
+ ├── models.py # Action and Observation models
250
+ └── server/
251
+ ├── __init__.py # Server module exports
252
+ ├── rag_optimizer_environment.py # Core environment logic
253
+ ├── app.py # FastAPI application (HTTP + WebSocket endpoints)
254
+ └── Dockerfile # Container image definition
255
+ ```
__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Rag Optimizer Environment."""
8
+
9
+ from .client import RagOptimizerEnv
10
+ from .models import RagOptimizerAction, RagOptimizerObservation
11
+
12
+ __all__ = [
13
+ "RagOptimizerAction",
14
+ "RagOptimizerObservation",
15
+ "RagOptimizerEnv",
16
+ ]
client.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Rag Optimizer Environment Client."""
8
+
9
+ from typing import Dict
10
+
11
+ from openenv.core import EnvClient
12
+ from openenv.core.client_types import StepResult
13
+ from openenv.core.env_server.types import State
14
+
15
+ from models import RagOptimizerAction, RagOptimizerObservation
16
+
17
+
18
+ class RagOptimizerEnvClient(EnvClient[RagOptimizerAction, RagOptimizerObservation, State]):
19
+ """
20
+ Client for the Rag Optimizer Environment.
21
+ Translates local Pydantic objects to JSON for the OpenEnv WebSocket.
22
+ """
23
+
24
+ def _step_payload(self, action: RagOptimizerAction) -> Dict:
25
+ """Convert RagOptimizerAction to JSON payload."""
26
+ return {
27
+ "action_type": action.action_type,
28
+ "doc_id": action.doc_id,
29
+ "text": action.text,
30
+ "metadata_key": action.metadata_key,
31
+ "metadata_value": action.metadata_value,
32
+ }
33
+
34
+ def _parse_result(self, payload: Dict) -> StepResult[RagOptimizerObservation]:
35
+ """Parse server response back into RagOptimizerObservation."""
36
+ obs_data = payload.get("observation", {})
37
+ observation = RagOptimizerObservation(
38
+ message=obs_data.get("message", ""),
39
+ current_docs=obs_data.get("current_docs", {}),
40
+ done=payload.get("done", False),
41
+ reward=payload.get("reward", 0.0),
42
+ metadata=obs_data.get("metadata", {}),
43
+ )
44
+
45
+ return StepResult(
46
+ observation=observation,
47
+ reward=payload.get("reward", 0.0),
48
+ done=payload.get("done", False),
49
+ )
50
+
51
+ def _parse_state(self, payload: Dict) -> State:
52
+ """Parse the hidden tracking state."""
53
+ return State(
54
+ episode_id=payload.get("episode_id"),
55
+ step_count=payload.get("step_count", 0),
56
+ )
inference.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import json
4
+ from openai import OpenAI
5
+
6
+ # Add the envs module to path so we can import client and models
7
+ sys.path.append(os.path.join(os.path.dirname(__file__), "envs", "rag_optimizer_env"))
8
+ from client import RagOptimizerEnvClient
9
+ from models import RagOptimizerAction
10
+
11
+ # Load environment variables
12
+ API_BASE_URL = os.getenv("API_BASE_URL")
13
+ MODEL_NAME = os.getenv("MODEL_NAME")
14
+ HF_TOKEN = os.getenv("HF_TOKEN")
15
+
16
+ MAX_STEPS = 30
17
+
18
+ SYSTEM_PROMPT = """You are an automated Data Engineer managing an AI Knowledge Base.
19
+ Your goal is to optimize the messy chunks of text in the database so that a TF-IDF Search Algorithm can find answers easily.
20
+ You must resolve contradictions, categorize documents, and delete unnecessary documents.
21
+
22
+ After each action you will receive a "current_reward" score (0.0 to 1.0) indicating how well the KB currently performs. Use this to guide your strategy.
23
+
24
+ You have the following actions:
25
+ - {"action_type": "read_document", "doc_id": "..."}
26
+ - {"action_type": "update_document", "doc_id": "...", "text": "..."}
27
+ - {"action_type": "delete_document", "doc_id": "..."}
28
+ - {"action_type": "add_metadata", "doc_id": "...", "metadata_key": "...", "metadata_value": "..."}
29
+ - {"action_type": "submit"}
30
+
31
+ You must return ONLY a raw JSON object detailing the action you want to take!"""
32
+
33
+ def format_action_str(action: RagOptimizerAction) -> str:
34
+ if action.action_type == "read_document":
35
+ return f"read('{action.doc_id}')"
36
+ elif action.action_type == "update_document":
37
+ return f"update('{action.doc_id}')"
38
+ elif action.action_type == "delete_document":
39
+ return f"delete('{action.doc_id}')"
40
+ elif action.action_type == "add_metadata":
41
+ return f"add_metadata('{action.doc_id}','{action.metadata_key}')"
42
+ elif action.action_type == "submit":
43
+ return "submit()"
44
+ return f"{action.action_type}()"
45
+
46
+ def main():
47
+ # Setup OpenAI Client
48
+ client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
49
+
50
+ # Track metrics for the final output
51
+ step_rewards = []
52
+ success = False
53
+ error_msg = "null"
54
+ score = 0.0
55
+
56
+ print(f"[START] task=rag_optimizer_env env=OpenEnv model={MODEL_NAME}")
57
+
58
+ # We suppress any other custom prints to respect the STDOUT format strictly
59
+ import contextlib
60
+ import io
61
+
62
+ with RagOptimizerEnvClient(base_url="http://localhost:8000").sync() as env:
63
+ # Suppress prints from client or env reset
64
+ with contextlib.redirect_stdout(io.StringIO()):
65
+ try:
66
+ result = env.reset()
67
+ observation = result.observation
68
+ except Exception as e:
69
+ error_msg = str(e).replace('\n', ' ')
70
+ print(f"[END] success=false steps=0 score=0.00 rewards=")
71
+ return
72
+
73
+ history = [{"role": "system", "content": SYSTEM_PROMPT}]
74
+
75
+ init_obs = {
76
+ "server_feedback": observation.message,
77
+ "current_reward": observation.reward,
78
+ "current_knowledge_base": observation.current_docs
79
+ }
80
+ history.append({"role": "user", "content": json.dumps(init_obs, indent=2)})
81
+
82
+ step = 0
83
+ for i in range(1, MAX_STEPS + 1):
84
+ step = i
85
+ messages = list(history)
86
+
87
+ action_str = "unknown"
88
+ error_msg = "null"
89
+
90
+ try:
91
+ completion = client.chat.completions.create(
92
+ model=MODEL_NAME,
93
+ messages=messages,
94
+ response_format={"type": "json_object"},
95
+ max_tokens=1000
96
+ )
97
+ response_text = completion.choices[0].message.content or ""
98
+ action_data = json.loads(response_text)
99
+
100
+ # Normalize fields if model returns lists instead of strings
101
+ for field in ("doc_id", "text", "metadata_key", "metadata_value"):
102
+ val = action_data.get(field)
103
+ if isinstance(val, list):
104
+ if val and isinstance(val[0], str):
105
+ action_data[field] = " ".join(val)
106
+ elif val and isinstance(val[0], dict):
107
+ action_data[field] = json.dumps(val[0])
108
+ else:
109
+ action_data[field] = str(val[0]) if val else ""
110
+
111
+ action = RagOptimizerAction(**action_data)
112
+ action_str = format_action_str(action)
113
+
114
+ except Exception as exc:
115
+ error_msg = str(exc).replace('\n', ' ')
116
+ action = RagOptimizerAction(action_type="submit")
117
+ action_str = format_action_str(action)
118
+
119
+ # Suppress normal prints during step
120
+ with contextlib.redirect_stdout(io.StringIO()):
121
+ try:
122
+ result = env.step(action)
123
+ observation = result.observation
124
+ reward = result.reward
125
+ except Exception as e:
126
+ error_msg = str(e).replace('\n', ' ')
127
+ reward = 0.0
128
+ result = type('obj', (object,), {'done': True})()
129
+ observation = type('obj', (object,), {'message': 'error', 'current_docs': {}})()
130
+
131
+ step_rewards.append(reward)
132
+ done = "true" if result.done else "false"
133
+
134
+ print(f"[STEP] step={step} action={action_str} reward={reward:.2f} done={done} error={error_msg}")
135
+
136
+ if result.done:
137
+ success = True if reward > 0.5 else False # Or however you define success
138
+ score = float(reward)
139
+ break
140
+
141
+ history.append({"role": "assistant", "content": json.dumps(action.model_dump(), default=str)})
142
+ next_obs = {
143
+ "server_feedback": observation.message,
144
+ "current_reward": observation.reward,
145
+ "current_knowledge_base": observation.current_docs
146
+ }
147
+ history.append({"role": "user", "content": json.dumps(next_obs, indent=2)})
148
+
149
+ else:
150
+ # Reached max steps
151
+ success = False
152
+ score = float(result.reward)
153
+
154
+ rewards_str = ",".join([f"{r:.2f}" for r in step_rewards])
155
+ done_str = "true" if success else "false"
156
+ print(f"[END] success={done_str} steps={step} score={score:.2f} rewards={rewards_str}")
157
+
158
+ if __name__ == "__main__":
159
+ main()
models.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Pydantic schemas for the Rag Optimizer environment.
9
+ These define the API contract between the client (agent) and the server.
10
+ """
11
+
12
+ from typing import Dict, Literal, Optional
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ class RagOptimizerAction(BaseModel):
17
+ """
18
+ Actions the agent can take to interact with the Knowledge Base.
19
+ """
20
+
21
+ action_type: Literal["read_document", "update_document", "delete_document", "add_metadata", "submit"] = Field(
22
+ ...,
23
+ description="The RAG optimization tool to execute."
24
+ )
25
+
26
+ doc_id: Optional[str] = Field(
27
+ None,
28
+ description="The ID of the document to target."
29
+ )
30
+ text: Optional[str] = Field(
31
+ None,
32
+ description="The text content (used for update_document)."
33
+ )
34
+ metadata_key: Optional[str] = Field(
35
+ None,
36
+ description="The key of the metadata tag (used for add_metadata)."
37
+ )
38
+ metadata_value: Optional[str] = Field(
39
+ None,
40
+ description="The value of the metadata tag (used for add_metadata)."
41
+ )
42
+
43
+
44
+ class RagOptimizerObservation(BaseModel):
45
+ """
46
+ The environment's response to an action, including the state of the KB.
47
+ """
48
+
49
+ message: str = Field(
50
+ ...,
51
+ description="Feedback from the last action (e.g., success/error messages)."
52
+ )
53
+
54
+ current_docs: Dict[str, Dict] = Field(
55
+ ...,
56
+ description="A live summary of the documents currently inside the KB (doc_id -> metadata/length)."
57
+ )
58
+
59
+ # Required OpenEnv standard fields
60
+ done: bool = Field(False, description="Whether the episode has finished.")
61
+ reward: float = Field(0.0, description="The reward obtained from the last step.")
62
+ metadata: Dict = Field(default_factory=dict, description="Additional optional information.")
openenv.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: rag_optimizer
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
7
+
pyproject.toml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ [build-system]
8
+ requires = ["setuptools>=45", "wheel"]
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [project]
12
+ name = "openenv-rag_optimizer"
13
+ version = "0.1.0"
14
+ description = "Rag Optimizer environment for OpenEnv"
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ # Core OpenEnv runtime (provides FastAPI server + HTTP client types)
18
+ # install from github
19
+ # "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
20
+ "openenv-core[core]>=0.2.2",
21
+ # Environment-specific dependencies
22
+ # Add all dependencies needed for your environment here
23
+ # Examples:
24
+ # "numpy>=1.19.0",
25
+ # "torch>=2.0.0",
26
+ # "gymnasium>=0.29.0",
27
+ # "openspiel>=1.0.0",
28
+ # "smolagents>=1.22.0,<2",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=8.0.0",
34
+ "pytest-cov>=4.0.0",
35
+ ]
36
+
37
+ [project.scripts]
38
+ # Server entry point - enables running via: uv run --project . server
39
+ # or: python -m rag_optimizer.server.app
40
+ server = "rag_optimizer.server.app:main"
41
+
42
+ [tool.setuptools]
43
+ include-package-data = true
44
+ packages = ["rag_optimizer", "rag_optimizer.server"]
45
+ package-dir = { "rag_optimizer" = ".", "rag_optimizer.server" = "server" }
server/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Rag Optimizer environment server components."""
8
+
9
+ from .rag_optimizer_environment import RagOptimizerEnvironment
10
+
11
+ __all__ = ["RagOptimizerEnvironment"]
server/app.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ FastAPI application for the Rag Optimizer Environment.
9
+
10
+ This module creates an HTTP server that exposes the RagOptimizerEnvironment
11
+ over HTTP and WebSocket endpoints, compatible with EnvClient.
12
+
13
+ Endpoints:
14
+ - POST /reset: Reset the environment
15
+ - POST /step: Execute an action
16
+ - GET /state: Get current environment state
17
+ - GET /schema: Get action/observation schemas
18
+ - WS /ws: WebSocket endpoint for persistent sessions
19
+
20
+ Usage:
21
+ # Development (with auto-reload):
22
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
23
+
24
+ # Production:
25
+ uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
26
+
27
+ # Or run directly:
28
+ python -m server.app
29
+ """
30
+
31
+ try:
32
+ from openenv.core.env_server.http_server import create_app
33
+ except Exception as e: # pragma: no cover
34
+ raise ImportError(
35
+ "openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
36
+ ) from e
37
+
38
+ try:
39
+ from models import RagOptimizerAction, RagOptimizerObservation
40
+ from .rag_optimizer_environment import RagOptimizerEnvironment
41
+ except ModuleNotFoundError:
42
+ from models import RagOptimizerAction, RagOptimizerObservation
43
+ from server.rag_optimizer_environment import RagOptimizerEnvironment
44
+
45
+
46
+ # Create the app with web interface and README integration
47
+ app = create_app(
48
+ RagOptimizerEnvironment,
49
+ RagOptimizerAction,
50
+ RagOptimizerObservation,
51
+ env_name="rag_optimizer",
52
+ max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
53
+ )
54
+
55
+
56
+ def main(host: str = "0.0.0.0", port: int = 8002):
57
+ """
58
+ Entry point for direct execution via uv run or python -m.
59
+
60
+ This function enables running the server without Docker:
61
+ uv run --project . server
62
+ uv run --project . server --port 8001
63
+ python -m rag_optimizer.server.app
64
+
65
+ Args:
66
+ host: Host address to bind to (default: "0.0.0.0")
67
+ port: Port number to listen on (default: 8000)
68
+
69
+ For production deployments, consider using uvicorn directly with
70
+ multiple workers:
71
+ uvicorn rag_optimizer.server.app:app --workers 4
72
+ """
73
+ import uvicorn
74
+
75
+ uvicorn.run(app, host=host, port=port)
76
+
77
+
78
+ if __name__ == '__main__':
79
+ main()
server/rag_optimizer_environment.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Rag Optimizer Environment Implementation.
9
+ The agent acts as a Data Engineer to un-block a broken RAG pipeline.
10
+ """
11
+
12
+ from uuid import uuid4
13
+ from typing import Dict, Any, List
14
+
15
+ from openenv.core.env_server.interfaces import Environment
16
+ from openenv.core.env_server.types import State
17
+
18
+ # Import scikit-learn for our Grader
19
+ from sklearn.feature_extraction.text import TfidfVectorizer
20
+ from sklearn.metrics.pairwise import cosine_similarity
21
+ import numpy as np
22
+
23
+ try:
24
+ from models import RagOptimizerAction, RagOptimizerObservation
25
+ except ImportError:
26
+ from models import RagOptimizerAction, RagOptimizerObservation
27
+
28
+
29
+ class RagOptimizerEnvironment(Environment):
30
+ """
31
+ RAG Optimizer Engine.
32
+ Maintains a simulated Knowledge Base and grades it using TF-IDF.
33
+ """
34
+
35
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
36
+
37
+ def __init__(self):
38
+ self._state = State(episode_id=str(uuid4()), step_count=0)
39
+
40
+ # Initial messy knowledge base
41
+ self.kb = {
42
+ "doc_pricing_legacy": {
43
+ "text": "Pricing for 2021: Enterprise tier is $1000/mo. Standard is $500/mo. All plans include 10 users.",
44
+ "metadata": {"type": "pricing"}
45
+ },
46
+ "doc_pricing_current_v2": {
47
+ "text": "Current Pricing 2024: Enterprise is $1500/mo. Standard is $750/mo. Refunds are not permitted on the enterprise tier.",
48
+ "metadata": {}
49
+ },
50
+ "doc_shipping_policy": {
51
+ "text": "All internal shipments to remote branch offices take 5-7 business days. Overnight shipping is only available for C-suite.",
52
+ "metadata": {"department": "logistics"}
53
+ },
54
+ "doc_messy_support_ticket_1": {
55
+ "text": "User complained the button disappeared on the frontend. Another user said the database latency was high. The frontend team fixed the button by updating CSS.",
56
+ "metadata": {}
57
+ },
58
+ "doc_messy_support_ticket_2": {
59
+ "text": "Email integration is failing with error 401 Unauthorized. The API key was rotated on Tuesday.",
60
+ "metadata": {}
61
+ },
62
+ "doc_monolithic_onboarding": {
63
+ "text": "Welcome to the company! Here are some rules. 1) VPN access requires DUO. 2) The cafetaria opens at 8 AM. 3) For HR issues, email hr@company.com. 4) The 2024 holiday schedule includes Dec 25, Jan 1, and July 4. 5) Parking passes must be renewed annually in March.",
64
+ "metadata": {}
65
+ },
66
+ # Add distractor files
67
+ **{f"doc_distractor_hr_{i}": {"text": f"This is an old HR policy document regarding {['pto', 'sick leave', 'travel', 'expenses'][i%4]} from 201{i%10}.", "metadata":{}} for i in range(10)},
68
+ **{f"doc_distractor_eng_{i}": {"text": f"Engineering architecture decision record {i}. We decided to use {['React', 'Postgres', 'Redis', 'Kafka'][i%4]} because of scaling concerns.", "metadata":{}} for i in range(10)},
69
+ **{f"doc_distractor_random_{i}": {"text": f"Weekly team update notes. Nothing important here, just discussed the weather and the upcoming launch {i}.", "metadata":{}} for i in range(10)},
70
+ }
71
+
72
+ # Hidden test suite for the grader
73
+ self.test_suite = [
74
+ {
75
+ "query": "What is the current 2024 price for standard?",
76
+ "target_concept": "750/mo"
77
+ },
78
+ {
79
+ "query": "What is the refund policy for enterprise?",
80
+ "target_concept": "Refunds are not permitted"
81
+ },
82
+ {
83
+ "query": "UI issues frontend CSS missing button",
84
+ "target_concept": "frontend team fixed the button"
85
+ },
86
+ {
87
+ "query": "How long does shipping take to branch offices?",
88
+ "target_concept": "5-7 business days"
89
+ },
90
+ {
91
+ "query": "What months do parking passes need to be renewed?",
92
+ "target_concept": "March"
93
+ },
94
+ {
95
+ "query": "What holidays are we off in 2024?",
96
+ "target_concept": "July 4"
97
+ }
98
+ ]
99
+
100
+ def _get_kb_summary(self) -> Dict[str, Dict]:
101
+ """Returns a summary of the KB for the observation."""
102
+ summary = {}
103
+ for k, v in self.kb.items():
104
+ summary[k] = {"metadata": v.get("metadata", {}), "length": len(v.get("text", ""))}
105
+ return summary
106
+
107
+ def reset(self) -> RagOptimizerObservation:
108
+ self._state = State(episode_id=str(uuid4()), step_count=0)
109
+ return RagOptimizerObservation(
110
+ message="RagOptimizerEnv Initialized. You have messy chunks in the KB. Resolve conflicts, add metadata tags to short tickets, and splinter monolithic files to win.",
111
+ current_docs=self._get_kb_summary(),
112
+ done=False,
113
+ reward=self._evaluate_kb()
114
+ )
115
+
116
+ def _evaluate_kb(self) -> float:
117
+ """The Grader: Evaluates the agent's current KB using TF-IDF."""
118
+ if not self.kb:
119
+ return 0.0
120
+
121
+ doc_texts = [doc["text"] for doc in self.kb.values()]
122
+
123
+ vectorizer = TfidfVectorizer(stop_words='english')
124
+ try:
125
+ doc_vectors = vectorizer.fit_transform(doc_texts)
126
+ except ValueError:
127
+ return 0.0
128
+
129
+ score = 0.0
130
+
131
+ for case in self.test_suite:
132
+ query_vec = vectorizer.transform([case["query"]])
133
+ similarities = cosine_similarity(query_vec, doc_vectors)[0]
134
+
135
+ # Get top 3
136
+ top_k_indices = similarities.argsort()[-3:][::-1]
137
+
138
+ found = False
139
+ for idx in top_k_indices:
140
+ if similarities[idx] > 0.01:
141
+ if case["target_concept"].lower() in doc_texts[idx].lower():
142
+ found = True
143
+ break
144
+ if found:
145
+ score += 1.0
146
+
147
+ return float(score / len(self.test_suite))
148
+
149
+ def step(self, action: RagOptimizerAction) -> RagOptimizerObservation: # type: ignore[override]
150
+ self._state.step_count += 1
151
+
152
+ msg = ""
153
+ done = False
154
+ reward = 0.0
155
+
156
+ try:
157
+ if action.action_type == "read_document":
158
+ if action.doc_id in self.kb:
159
+ msg = f"Content of {action.doc_id}: {self.kb[action.doc_id]['text']}"
160
+ else:
161
+ msg = f"Error: doc_id {action.doc_id} not found."
162
+
163
+ elif action.action_type == "delete_document":
164
+ if action.doc_id in self.kb:
165
+ del self.kb[action.doc_id]
166
+ msg = f"Deleted {action.doc_id}."
167
+ else:
168
+ msg = f"Error: doc_id {action.doc_id} not found."
169
+
170
+ elif action.action_type == "update_document":
171
+ if not action.doc_id or not action.text:
172
+ msg = "Error: doc_id and text required for update_document."
173
+ else:
174
+ if action.doc_id not in self.kb:
175
+ self.kb[action.doc_id] = {"text": "", "metadata": {}}
176
+ self.kb[action.doc_id]["text"] = action.text
177
+ msg = f"Updated text for {action.doc_id}."
178
+
179
+ elif action.action_type == "add_metadata":
180
+ if not action.doc_id or not action.metadata_key or not action.metadata_value:
181
+ msg = "Error: doc_id, metadata_key, and metadata_value required."
182
+ else:
183
+ if action.doc_id not in self.kb:
184
+ msg = f"Error: doc_id {action.doc_id} not found."
185
+ else:
186
+ self.kb[action.doc_id]["metadata"][action.metadata_key] = action.metadata_value
187
+ msg = f"Added metadata to {action.doc_id}."
188
+
189
+ elif action.action_type == "submit":
190
+ done = True
191
+ reward = self._evaluate_kb()
192
+ msg = f"Evaluation complete. Final reward: {reward:.2f}"
193
+
194
+ except Exception as e:
195
+ msg = f"Action failed: {str(e)}"
196
+
197
+ if not done:
198
+ reward = self._evaluate_kb()
199
+
200
+ return RagOptimizerObservation(
201
+ message=msg,
202
+ current_docs=self._get_kb_summary(),
203
+ done=done,
204
+ reward=reward,
205
+ )
206
+
207
+ @property
208
+ def state(self) -> State:
209
+ return self._state
server/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ openenv[core]>=0.2.0
2
+ fastapi>=0.115.0
3
+ uvicorn>=0.24.0
4
+ scikit-learn>=1.3.0
uv.lock ADDED
The diff for this file is too large to render. See raw diff