Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +16 -16
- inference.py +15 -12
- openenv.yaml +4 -1
- openenv_token_optimiser.egg-info/SOURCES.txt +4 -0
- server/app.py +12 -9
- server/token_optimiser_environment.py +58 -26
- validator_script.sh +185 -0
README.md
CHANGED
|
@@ -80,22 +80,22 @@ class TokenOptimiserState(State):
|
|
| 80 |
|
| 81 |
## 📋 Tasks
|
| 82 |
|
| 83 |
-
### 🟢 Easy —
|
| 84 |
-
**Original:** `"
|
| 85 |
-
**Goal:** Strip filler
|
| 86 |
-
**Expected optimized:** `"
|
| 87 |
-
**Max output:**
|
| 88 |
-
|
| 89 |
-
### 🟡 Medium —
|
| 90 |
-
**Original:** `"I
|
| 91 |
-
**Goal:** Compress input AND
|
| 92 |
-
**Expected optimized:** `"
|
| 93 |
-
**Max output:**
|
| 94 |
-
|
| 95 |
-
### 🔴 Hard — Multi-
|
| 96 |
-
**Original:**
|
| 97 |
-
**Goal:**
|
| 98 |
-
**Expected optimized:**
|
| 99 |
**Max output:** 200 tokens
|
| 100 |
|
| 101 |
---
|
|
|
|
| 80 |
|
| 81 |
## 📋 Tasks
|
| 82 |
|
| 83 |
+
### 🟢 Easy — Redundancy Stripping
|
| 84 |
+
**Original:** `"Could you possibly help me understand, if it's not too much trouble, what the word 'photosynthesis' means? I would really appreciate it if you could explain it to me in simple terms that are easy to understand."`
|
| 85 |
+
**Goal:** Strip politeness filler and redundancy to a single direct question without formatting
|
| 86 |
+
**Expected optimized:** `"What does photosynthesis mean? Be brief."`
|
| 87 |
+
**Max output:** 30 tokens
|
| 88 |
+
|
| 89 |
+
### 🟡 Medium — Constraint Injection
|
| 90 |
+
**Original:** `"I'm looking for information about the main differences between Python and JavaScript programming languages. Could you give me a thorough breakdown covering things like typing, use cases, performance, syntax style, and ecosystem so I can decide which one to learn first?"`
|
| 91 |
+
**Goal:** Compress input AND inject format + exactly 5 bullet point counts into prompt
|
| 92 |
+
**Expected optimized:** `"Compare Python and JavaScript (typing, use cases, performance, syntax, ecosystem) in exactly 5 bullet points."`
|
| 93 |
+
**Max output:** 120 tokens
|
| 94 |
+
|
| 95 |
+
### 🔴 Hard — Multi-Key JSON Extraction
|
| 96 |
+
**Original:** `"We need you to analyze our e-commerce platform data and provide strategic insights. Specifically: first identify which product categories are performing best by revenue, second tell us which geographic regions show the most growth potential, third identify which customer segments respond best to promotions, fourth suggest how we should allocate our Q3 marketing budget across channels, and fifth flag any market risks we should be watching. Please be thorough in your analysis and provide detailed reasoning for each point."`
|
| 97 |
+
**Goal:** Compress 82-word multi-intent prompt and force structured JSON output with 5 exact required keys
|
| 98 |
+
**Expected optimized:** `"Analyze e-commerce data based on revenue, growth regions, responsive segments, Q3 budget, and market risks. Output strictly as JSON with keys: top_categories, growth_regions, responsive_segments, budget_allocation, risks_watch."`
|
| 99 |
**Max output:** 200 tokens
|
| 100 |
|
| 101 |
---
|
inference.py
CHANGED
|
@@ -15,6 +15,7 @@ Environment variables required:
|
|
| 15 |
"""
|
| 16 |
|
| 17 |
import asyncio
|
|
|
|
| 18 |
import os
|
| 19 |
import textwrap
|
| 20 |
from typing import List, Optional
|
|
@@ -27,6 +28,14 @@ except Exception: # pragma: no cover
|
|
| 27 |
|
| 28 |
from token_optimiser import TokenOptimiserEnv, TokenOptimiserAction
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# ---------------------------------------------------------------------------
|
| 31 |
# Configuration
|
| 32 |
# ---------------------------------------------------------------------------
|
|
@@ -151,7 +160,7 @@ def get_optimized_prompt(
|
|
| 151 |
result = (completion.choices[0].message.content or "").strip()
|
| 152 |
return result if result else "Explain briefly."
|
| 153 |
except Exception as exc:
|
| 154 |
-
|
| 155 |
return _rule_based_compress(original_prompt, step)
|
| 156 |
|
| 157 |
|
|
@@ -210,11 +219,8 @@ async def run_episode(llm: OpenAI) -> None:
|
|
| 210 |
env_state = await env.state()
|
| 211 |
original_prompt: str = env_state.original_prompt or "Explain machine learning briefly."
|
| 212 |
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
f"Original prompt ({len(original_prompt.split())} words): {original_prompt[:80]}...",
|
| 216 |
-
flush=True,
|
| 217 |
-
)
|
| 218 |
|
| 219 |
prev_reward = 0.0
|
| 220 |
prev_response = ""
|
|
@@ -236,10 +242,7 @@ async def run_episode(llm: OpenAI) -> None:
|
|
| 236 |
reward = result.reward # server puts reward at top-level, not inside obs
|
| 237 |
done = result.done or (step >= MAX_STEPS)
|
| 238 |
prev_response = obs.llm_response
|
| 239 |
-
|
| 240 |
-
f"[DEBUG] tokens in={obs.input_tokens} out={obs.output_tokens}",
|
| 241 |
-
flush=True,
|
| 242 |
-
)
|
| 243 |
except Exception as exc:
|
| 244 |
error_msg = str(exc)
|
| 245 |
done = True
|
|
@@ -263,7 +266,7 @@ async def run_episode(llm: OpenAI) -> None:
|
|
| 263 |
success = score >= SUCCESS_THRESHOLD
|
| 264 |
|
| 265 |
except Exception as exc:
|
| 266 |
-
|
| 267 |
finally:
|
| 268 |
try:
|
| 269 |
await env.close()
|
|
@@ -274,7 +277,7 @@ async def run_episode(llm: OpenAI) -> None:
|
|
| 274 |
|
| 275 |
async def main() -> None:
|
| 276 |
if not HF_TOKEN:
|
| 277 |
-
|
| 278 |
return
|
| 279 |
|
| 280 |
llm = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
|
|
|
|
| 15 |
"""
|
| 16 |
|
| 17 |
import asyncio
|
| 18 |
+
import logging
|
| 19 |
import os
|
| 20 |
import textwrap
|
| 21 |
from typing import List, Optional
|
|
|
|
| 28 |
|
| 29 |
from token_optimiser import TokenOptimiserEnv, TokenOptimiserAction
|
| 30 |
|
| 31 |
+
logger = logging.getLogger("TokenOptimiserFrontend")
|
| 32 |
+
logger.setLevel(logging.INFO)
|
| 33 |
+
if not logger.handlers:
|
| 34 |
+
handler = logging.StreamHandler()
|
| 35 |
+
formatter = logging.Formatter('\033[96m%(asctime)s\033[0m | \033[93m%(levelname)-7s\033[0m | \033[1mCLIENT\033[0m | %(message)s', datefmt='%H:%M:%S')
|
| 36 |
+
handler.setFormatter(formatter)
|
| 37 |
+
logger.addHandler(handler)
|
| 38 |
+
|
| 39 |
# ---------------------------------------------------------------------------
|
| 40 |
# Configuration
|
| 41 |
# ---------------------------------------------------------------------------
|
|
|
|
| 160 |
result = (completion.choices[0].message.content or "").strip()
|
| 161 |
return result if result else "Explain briefly."
|
| 162 |
except Exception as exc:
|
| 163 |
+
logger.warning(f"LLM call failed, falling back to basic rule compression: {exc}")
|
| 164 |
return _rule_based_compress(original_prompt, step)
|
| 165 |
|
| 166 |
|
|
|
|
| 219 |
env_state = await env.state()
|
| 220 |
original_prompt: str = env_state.original_prompt or "Explain machine learning briefly."
|
| 221 |
|
| 222 |
+
logger.info(f"Task connected. Difficulty: {env_state.task_difficulty.upper()}")
|
| 223 |
+
logger.info(f"Original prompt ({len(original_prompt.split())} words): {original_prompt[:80]}...")
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
prev_reward = 0.0
|
| 226 |
prev_response = ""
|
|
|
|
| 242 |
reward = result.reward # server puts reward at top-level, not inside obs
|
| 243 |
done = result.done or (step >= MAX_STEPS)
|
| 244 |
prev_response = obs.llm_response
|
| 245 |
+
logger.info(f"Step {step} Tokens => Input: {obs.input_tokens}, Output: {obs.output_tokens}")
|
|
|
|
|
|
|
|
|
|
| 246 |
except Exception as exc:
|
| 247 |
error_msg = str(exc)
|
| 248 |
done = True
|
|
|
|
| 266 |
success = score >= SUCCESS_THRESHOLD
|
| 267 |
|
| 268 |
except Exception as exc:
|
| 269 |
+
logger.error(f"Episode error aborted run: {exc}")
|
| 270 |
finally:
|
| 271 |
try:
|
| 272 |
await env.close()
|
|
|
|
| 277 |
|
| 278 |
async def main() -> None:
|
| 279 |
if not HF_TOKEN:
|
| 280 |
+
logger.error("HF_TOKEN environment variable not set. Exiting.")
|
| 281 |
return
|
| 282 |
|
| 283 |
llm = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
|
openenv.yaml
CHANGED
|
@@ -4,4 +4,7 @@ type: space
|
|
| 4 |
runtime: fastapi
|
| 5 |
app: server.app:app
|
| 6 |
port: 8000
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
runtime: fastapi
|
| 5 |
app: server.app:app
|
| 6 |
port: 8000
|
| 7 |
+
tasks:
|
| 8 |
+
- redundancy_stripping (easy)
|
| 9 |
+
- constraint_injection (medium)
|
| 10 |
+
- multi_key_json_extraction (hard)
|
openenv_token_optimiser.egg-info/SOURCES.txt
CHANGED
|
@@ -1,4 +1,8 @@
|
|
| 1 |
README.md
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
pyproject.toml
|
| 3 |
./__init__.py
|
| 4 |
./client.py
|
|
|
|
| 1 |
README.md
|
| 2 |
+
__init__.py
|
| 3 |
+
client.py
|
| 4 |
+
inference.py
|
| 5 |
+
models.py
|
| 6 |
pyproject.toml
|
| 7 |
./__init__.py
|
| 8 |
./client.py
|
server/app.py
CHANGED
|
@@ -54,7 +54,7 @@ app = create_app(
|
|
| 54 |
)
|
| 55 |
|
| 56 |
|
| 57 |
-
def main(
|
| 58 |
"""
|
| 59 |
Entry point for direct execution via uv run or python -m.
|
| 60 |
|
|
@@ -63,20 +63,23 @@ def main() -> None:
|
|
| 63 |
uv run --project . server --port 8001
|
| 64 |
python -m token_optimiser.server.app
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
For production deployments, consider using uvicorn directly with
|
| 67 |
multiple workers:
|
| 68 |
uvicorn token_optimiser.server.app:app --workers 4
|
| 69 |
"""
|
| 70 |
-
import argparse
|
| 71 |
import uvicorn
|
| 72 |
|
| 73 |
-
|
| 74 |
-
parser.add_argument("--host", type=str, default="0.0.0.0")
|
| 75 |
-
parser.add_argument("--port", type=int, default=8000)
|
| 76 |
-
args = parser.parse_args()
|
| 77 |
-
|
| 78 |
-
uvicorn.run(app, host=args.host, port=args.port)
|
| 79 |
|
| 80 |
|
| 81 |
if __name__ == "__main__":
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
)
|
| 55 |
|
| 56 |
|
| 57 |
+
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 58 |
"""
|
| 59 |
Entry point for direct execution via uv run or python -m.
|
| 60 |
|
|
|
|
| 63 |
uv run --project . server --port 8001
|
| 64 |
python -m token_optimiser.server.app
|
| 65 |
|
| 66 |
+
Args:
|
| 67 |
+
host: Host address to bind to (default: "0.0.0.0")
|
| 68 |
+
port: Port number to listen on (default: 8000)
|
| 69 |
+
|
| 70 |
For production deployments, consider using uvicorn directly with
|
| 71 |
multiple workers:
|
| 72 |
uvicorn token_optimiser.server.app:app --workers 4
|
| 73 |
"""
|
|
|
|
| 74 |
import uvicorn
|
| 75 |
|
| 76 |
+
uvicorn.run(app, host=host, port=port)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
if __name__ == "__main__":
|
| 80 |
+
import argparse
|
| 81 |
+
|
| 82 |
+
parser = argparse.ArgumentParser()
|
| 83 |
+
parser.add_argument("--port", type=int, default=8000)
|
| 84 |
+
args = parser.parse_args()
|
| 85 |
+
main(port=args.port)
|
server/token_optimiser_environment.py
CHANGED
|
@@ -11,10 +11,22 @@ A sandboxed LLM interaction environment where an AI agent optimizes both input p
|
|
| 11 |
and expected output responses to minimize total token usage while maintaining correctness.
|
| 12 |
"""
|
| 13 |
|
|
|
|
|
|
|
| 14 |
import os
|
| 15 |
import random
|
|
|
|
| 16 |
from uuid import uuid4
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
try:
|
| 19 |
from openai import OpenAI
|
| 20 |
except ImportError:
|
|
@@ -62,29 +74,30 @@ class TokenOptimiserEnvironment(Environment):
|
|
| 62 |
# EASY TASK
|
| 63 |
{
|
| 64 |
"difficulty": "easy",
|
| 65 |
-
"prompt": "
|
| 66 |
-
"expected_format": "
|
| 67 |
-
"reference_response": "
|
| 68 |
-
"max_output_tokens":
|
| 69 |
-
"description": "
|
| 70 |
},
|
| 71 |
# MEDIUM TASK
|
| 72 |
{
|
| 73 |
"difficulty": "medium",
|
| 74 |
-
"prompt": "I
|
| 75 |
-
"expected_format": "
|
| 76 |
-
"reference_response": "•
|
| 77 |
-
"max_output_tokens":
|
| 78 |
-
"description": "Compress input
|
| 79 |
},
|
| 80 |
# HARD TASK
|
| 81 |
{
|
| 82 |
"difficulty": "hard",
|
| 83 |
-
"prompt": "
|
| 84 |
-
"expected_format": "
|
| 85 |
-
"reference_response": '{"top_categories":
|
|
|
|
| 86 |
"max_output_tokens": 200,
|
| 87 |
-
"description": "
|
| 88 |
}
|
| 89 |
]
|
| 90 |
|
|
@@ -106,6 +119,11 @@ class TokenOptimiserEnvironment(Environment):
|
|
| 106 |
)
|
| 107 |
self._reset_count += 1
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
return TokenOptimiserObservation(
|
| 110 |
llm_response="",
|
| 111 |
input_tokens=0,
|
|
@@ -143,12 +161,24 @@ class TokenOptimiserEnvironment(Environment):
|
|
| 143 |
# 4. Format compliance (0.0-0.2)
|
| 144 |
expected_fmt = self._current_task["expected_format"]
|
| 145 |
format_score = 0.0
|
| 146 |
-
if
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
# 5. Length penalty if output way too long
|
| 154 |
max_out = self._current_task["max_output_tokens"]
|
|
@@ -163,10 +193,11 @@ class TokenOptimiserEnvironment(Environment):
|
|
| 163 |
)
|
| 164 |
reward = max(0.0, min(1.0, reward))
|
| 165 |
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
| 170 |
)
|
| 171 |
|
| 172 |
return TokenOptimiserObservation(
|
|
@@ -196,13 +227,14 @@ class TokenOptimiserEnvironment(Environment):
|
|
| 196 |
return text, in_tok, out_tok
|
| 197 |
except Exception as e:
|
| 198 |
if "429" in str(e) or "Too Many Requests" in str(e):
|
| 199 |
-
|
| 200 |
time.sleep(3)
|
| 201 |
else:
|
| 202 |
-
|
| 203 |
break
|
| 204 |
|
| 205 |
# Rule-based fallback
|
|
|
|
| 206 |
return self._fallback_simulate(prompt)
|
| 207 |
|
| 208 |
def _fallback_simulate(self, prompt: str) -> tuple[str, int, int]:
|
|
|
|
| 11 |
and expected output responses to minimize total token usage while maintaining correctness.
|
| 12 |
"""
|
| 13 |
|
| 14 |
+
import json
|
| 15 |
+
import logging
|
| 16 |
import os
|
| 17 |
import random
|
| 18 |
+
import re
|
| 19 |
from uuid import uuid4
|
| 20 |
|
| 21 |
+
logger = logging.getLogger("TokenOptimiserBackend")
|
| 22 |
+
logger.setLevel(logging.INFO)
|
| 23 |
+
if not logger.handlers:
|
| 24 |
+
handler = logging.StreamHandler()
|
| 25 |
+
# Add clear ANSI color prefixes for visibility in backend terminal
|
| 26 |
+
formatter = logging.Formatter('\033[94m%(asctime)s\033[0m | \033[92m%(levelname)-7s\033[0m | \033[1m%(message)s\033[0m', datefmt='%H:%M:%S')
|
| 27 |
+
handler.setFormatter(formatter)
|
| 28 |
+
logger.addHandler(handler)
|
| 29 |
+
|
| 30 |
try:
|
| 31 |
from openai import OpenAI
|
| 32 |
except ImportError:
|
|
|
|
| 74 |
# EASY TASK
|
| 75 |
{
|
| 76 |
"difficulty": "easy",
|
| 77 |
+
"prompt": "Could you possibly help me understand, if it's not too much trouble, what the word 'photosynthesis' means? I would really appreciate it if you could explain it to me in simple terms that are easy to understand.",
|
| 78 |
+
"expected_format": "plain_brief",
|
| 79 |
+
"reference_response": "Photosynthesis is how plants convert sunlight into food using CO2 and water.",
|
| 80 |
+
"max_output_tokens": 30,
|
| 81 |
+
"description": "Strip politeness filler and redundancy to a single direct question"
|
| 82 |
},
|
| 83 |
# MEDIUM TASK
|
| 84 |
{
|
| 85 |
"difficulty": "medium",
|
| 86 |
+
"prompt": "I'm looking for information about the main differences between Python and JavaScript programming languages. Could you give me a thorough breakdown covering things like typing, use cases, performance, syntax style, and ecosystem so I can decide which one to learn first?",
|
| 87 |
+
"expected_format": "bullet_5",
|
| 88 |
+
"reference_response": "• Python: dynamic typing, data/ML focus\n• JS: dynamic typing, web/frontend focus\n• Performance: JS V8 faster for runtime\n• Syntax: Python readable, JS C-like\n• Ecosystem: Python pip/sci libs, JS npm/frameworks",
|
| 89 |
+
"max_output_tokens": 120,
|
| 90 |
+
"description": "Compress input AND inject format + count constraint into prompt"
|
| 91 |
},
|
| 92 |
# HARD TASK
|
| 93 |
{
|
| 94 |
"difficulty": "hard",
|
| 95 |
+
"prompt": "We need you to analyze our e-commerce platform data and provide strategic insights. Specifically: first identify which product categories are performing best by revenue, second tell us which geographic regions show the most growth potential, third identify which customer segments respond best to promotions, fourth suggest how we should allocate our Q3 marketing budget across channels, and fifth flag any market risks we should be watching. Please be thorough in your analysis and provide detailed reasoning for each point.",
|
| 96 |
+
"expected_format": "json_5keys",
|
| 97 |
+
"reference_response": '{"top_categories":"...","growth_regions":"...","responsive_segments":"...","budget_allocation":"...","risks_watch":"..."}',
|
| 98 |
+
"required_json_keys": ["top_categories", "growth_regions", "responsive_segments", "budget_allocation", "risks_watch"],
|
| 99 |
"max_output_tokens": 200,
|
| 100 |
+
"description": "Compress 82-word multi-intent prompt and force structured JSON output with 5 exact keys"
|
| 101 |
}
|
| 102 |
]
|
| 103 |
|
|
|
|
| 119 |
)
|
| 120 |
self._reset_count += 1
|
| 121 |
|
| 122 |
+
logger.info(f"------ ENVIRONMENT RESET ------")
|
| 123 |
+
logger.info(f"Loaded Task: [{self._current_task['difficulty'].upper()}] Index: {self._state.task_index}")
|
| 124 |
+
logger.info(f"Requirements: Format='{self._current_task['expected_format']}', Max Tokens={self._current_task['max_output_tokens']}")
|
| 125 |
+
logger.info(f"-------------------------------")
|
| 126 |
+
|
| 127 |
return TokenOptimiserObservation(
|
| 128 |
llm_response="",
|
| 129 |
input_tokens=0,
|
|
|
|
| 161 |
# 4. Format compliance (0.0-0.2)
|
| 162 |
expected_fmt = self._current_task["expected_format"]
|
| 163 |
format_score = 0.0
|
| 164 |
+
if expected_fmt == "bullet_5":
|
| 165 |
+
bullet_count = llm_response.count('•')
|
| 166 |
+
if bullet_count >= 5:
|
| 167 |
+
format_score = 0.2
|
| 168 |
+
elif 3 <= bullet_count <= 4:
|
| 169 |
+
format_score = 0.1
|
| 170 |
+
elif expected_fmt == "json_5keys":
|
| 171 |
+
try:
|
| 172 |
+
parsed = json.loads(llm_response.strip())
|
| 173 |
+
keys_present = sum(1 for k in self._current_task["required_json_keys"] if k in parsed)
|
| 174 |
+
format_score = 0.04 * keys_present
|
| 175 |
+
except json.JSONDecodeError:
|
| 176 |
+
format_score = 0.0
|
| 177 |
+
elif expected_fmt == "plain_brief":
|
| 178 |
+
sentences = len([s for s in re.split(r'[.!?]+', llm_response) if s.strip()])
|
| 179 |
+
has_no_bullets = not any(c in llm_response for c in ("•", "-", "*"))
|
| 180 |
+
if sentences <= 2 and has_no_bullets:
|
| 181 |
+
format_score = 0.2
|
| 182 |
|
| 183 |
# 5. Length penalty if output way too long
|
| 184 |
max_out = self._current_task["max_output_tokens"]
|
|
|
|
| 193 |
)
|
| 194 |
reward = max(0.0, min(1.0, reward))
|
| 195 |
|
| 196 |
+
logger.info(f"[STEP {self._state.step_count}] Optimized Prompt Length: {len(optimized_prompt.split())} words")
|
| 197 |
+
logger.info(f" └─ Tokens => In: {int(input_tokens)}, Out: {int(output_tokens)}")
|
| 198 |
+
logger.info(
|
| 199 |
+
f" └─ Reward => Tok_Eff:{token_efficiency:.2f} | Semantic:{semantic_score*0.3:.2f} | "
|
| 200 |
+
f"Fmt:{format_score:.2f} | Penalty:{length_penalty:.2f} || TOTAL: {reward:.3f}"
|
| 201 |
)
|
| 202 |
|
| 203 |
return TokenOptimiserObservation(
|
|
|
|
| 227 |
return text, in_tok, out_tok
|
| 228 |
except Exception as e:
|
| 229 |
if "429" in str(e) or "Too Many Requests" in str(e):
|
| 230 |
+
logger.warning(f"Rate limited, waiting 3s (attempt {attempt+1})...")
|
| 231 |
time.sleep(3)
|
| 232 |
else:
|
| 233 |
+
logger.error(f"LLM call failed: {e}")
|
| 234 |
break
|
| 235 |
|
| 236 |
# Rule-based fallback
|
| 237 |
+
logger.debug("Falling back to rule-based simulation.")
|
| 238 |
return self._fallback_simulate(prompt)
|
| 239 |
|
| 240 |
def _fallback_simulate(self, prompt: str) -> tuple[str, int, int]:
|
validator_script.sh
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
#
|
| 3 |
+
# validate-submission.sh — OpenEnv Submission Validator
|
| 4 |
+
#
|
| 5 |
+
# Checks that your HF Space is live, Docker image builds, and openenv validate passes.
|
| 6 |
+
#
|
| 7 |
+
# Prerequisites:
|
| 8 |
+
# - Docker: https://docs.docker.com/get-docker/
|
| 9 |
+
# - openenv-core: pip install openenv-core
|
| 10 |
+
# - curl (usually pre-installed)
|
| 11 |
+
#
|
| 12 |
+
# Run:
|
| 13 |
+
# curl -fsSL https://raw.githubusercontent.com/<owner>/<repo>/main/scripts/validate-submission.sh | bash -s -- <ping_url> [repo_dir]
|
| 14 |
+
#
|
| 15 |
+
# Or download and run locally:
|
| 16 |
+
# chmod +x validate-submission.sh
|
| 17 |
+
# ./validate-submission.sh <ping_url> [repo_dir]
|
| 18 |
+
#
|
| 19 |
+
# Arguments:
|
| 20 |
+
# ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)
|
| 21 |
+
# repo_dir Path to your repo (default: current directory)
|
| 22 |
+
#
|
| 23 |
+
# Examples:
|
| 24 |
+
# ./validate-submission.sh https://my-team.hf.space
|
| 25 |
+
# ./validate-submission.sh https://my-team.hf.space ./my-repo
|
| 26 |
+
#
|
| 27 |
+
|
| 28 |
+
set -uo pipefail
|
| 29 |
+
|
| 30 |
+
DOCKER_BUILD_TIMEOUT=600
|
| 31 |
+
if [ -t 1 ]; then
|
| 32 |
+
RED='\033[0;31m'
|
| 33 |
+
GREEN='\033[0;32m'
|
| 34 |
+
YELLOW='\033[1;33m'
|
| 35 |
+
BOLD='\033[1m'
|
| 36 |
+
NC='\033[0m'
|
| 37 |
+
else
|
| 38 |
+
RED='' GREEN='' YELLOW='' BOLD='' NC=''
|
| 39 |
+
fi
|
| 40 |
+
|
| 41 |
+
run_with_timeout() {
|
| 42 |
+
local secs="$1"; shift
|
| 43 |
+
if command -v timeout &>/dev/null; then
|
| 44 |
+
timeout "$secs" "$@"
|
| 45 |
+
elif command -v gtimeout &>/dev/null; then
|
| 46 |
+
gtimeout "$secs" "$@"
|
| 47 |
+
else
|
| 48 |
+
"$@" &
|
| 49 |
+
local pid=$!
|
| 50 |
+
( sleep "$secs" && kill "$pid" 2>/dev/null ) &
|
| 51 |
+
local watcher=$!
|
| 52 |
+
wait "$pid" 2>/dev/null
|
| 53 |
+
local rc=$?
|
| 54 |
+
kill "$watcher" 2>/dev/null
|
| 55 |
+
wait "$watcher" 2>/dev/null
|
| 56 |
+
return $rc
|
| 57 |
+
fi
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
portable_mktemp() {
|
| 61 |
+
local prefix="${1:-validate}"
|
| 62 |
+
mktemp "${TMPDIR:-/tmp}/${prefix}-XXXXXX" 2>/dev/null || mktemp
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
CLEANUP_FILES=()
|
| 66 |
+
cleanup() { rm -f "${CLEANUP_FILES[@]+"${CLEANUP_FILES[@]}"}"; }
|
| 67 |
+
trap cleanup EXIT
|
| 68 |
+
|
| 69 |
+
PING_URL="${1:-}"
|
| 70 |
+
REPO_DIR="${2:-.}"
|
| 71 |
+
|
| 72 |
+
if [ -z "$PING_URL" ]; then
|
| 73 |
+
printf "Usage: %s <ping_url> [repo_dir]\n" "$0"
|
| 74 |
+
printf "\n"
|
| 75 |
+
printf " ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)\n"
|
| 76 |
+
printf " repo_dir Path to your repo (default: current directory)\n"
|
| 77 |
+
exit 1
|
| 78 |
+
fi
|
| 79 |
+
|
| 80 |
+
if ! REPO_DIR="$(cd "$REPO_DIR" 2>/dev/null && pwd)"; then
|
| 81 |
+
printf "Error: directory '%s' not found\n" "${2:-.}"
|
| 82 |
+
exit 1
|
| 83 |
+
fi
|
| 84 |
+
PING_URL="${PING_URL%/}"
|
| 85 |
+
export PING_URL
|
| 86 |
+
PASS=0
|
| 87 |
+
|
| 88 |
+
log() { printf "[%s] %b\n" "$(date -u +%H:%M:%S)" "$*"; }
|
| 89 |
+
pass() { log "${GREEN}PASSED${NC} -- $1"; PASS=$((PASS + 1)); }
|
| 90 |
+
fail() { log "${RED}FAILED${NC} -- $1"; }
|
| 91 |
+
hint() { printf " ${YELLOW}Hint:${NC} %b\n" "$1"; }
|
| 92 |
+
stop_at() {
|
| 93 |
+
printf "\n"
|
| 94 |
+
printf "${RED}${BOLD}Validation stopped at %s.${NC} Fix the above before continuing.\n" "$1"
|
| 95 |
+
exit 1
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
printf "\n"
|
| 99 |
+
printf "${BOLD}========================================${NC}\n"
|
| 100 |
+
printf "${BOLD} OpenEnv Submission Validator${NC}\n"
|
| 101 |
+
printf "${BOLD}========================================${NC}\n"
|
| 102 |
+
log "Repo: $REPO_DIR"
|
| 103 |
+
log "Ping URL: $PING_URL"
|
| 104 |
+
printf "\n"
|
| 105 |
+
|
| 106 |
+
log "${BOLD}Step 1/3: Pinging HF Space${NC} ($PING_URL/reset) ..."
|
| 107 |
+
|
| 108 |
+
CURL_OUTPUT=$(portable_mktemp "validate-curl")
|
| 109 |
+
CLEANUP_FILES+=("$CURL_OUTPUT")
|
| 110 |
+
HTTP_CODE=$(curl -s -o "$CURL_OUTPUT" -w "%{http_code}" -X POST \
|
| 111 |
+
-H "Content-Type: application/json" -d '{}' \
|
| 112 |
+
"$PING_URL/reset" --max-time 30 2>"$CURL_OUTPUT" || printf "000")
|
| 113 |
+
|
| 114 |
+
if [ "$HTTP_CODE" = "200" ]; then
|
| 115 |
+
pass "HF Space is live and responds to /reset"
|
| 116 |
+
elif [ "$HTTP_CODE" = "000" ]; then
|
| 117 |
+
fail "HF Space not reachable (connection failed or timed out)"
|
| 118 |
+
hint "Check your network connection and that the Space is running."
|
| 119 |
+
hint "Try: curl -s -o /dev/null -w '%%{http_code}' -X POST $PING_URL/reset"
|
| 120 |
+
stop_at "Step 1"
|
| 121 |
+
else
|
| 122 |
+
fail "HF Space /reset returned HTTP $HTTP_CODE (expected 200)"
|
| 123 |
+
hint "Make sure your Space is running and the URL is correct."
|
| 124 |
+
hint "Try opening $PING_URL in your browser first."
|
| 125 |
+
stop_at "Step 1"
|
| 126 |
+
fi
|
| 127 |
+
|
| 128 |
+
log "${BOLD}Step 2/3: Running docker build${NC} ..."
|
| 129 |
+
|
| 130 |
+
if ! command -v docker &>/dev/null; then
|
| 131 |
+
fail "docker command not found"
|
| 132 |
+
hint "Install Docker: https://docs.docker.com/get-docker/"
|
| 133 |
+
stop_at "Step 2"
|
| 134 |
+
fi
|
| 135 |
+
|
| 136 |
+
if [ -f "$REPO_DIR/Dockerfile" ]; then
|
| 137 |
+
DOCKER_CONTEXT="$REPO_DIR"
|
| 138 |
+
elif [ -f "$REPO_DIR/server/Dockerfile" ]; then
|
| 139 |
+
DOCKER_CONTEXT="$REPO_DIR/server"
|
| 140 |
+
else
|
| 141 |
+
fail "No Dockerfile found in repo root or server/ directory"
|
| 142 |
+
stop_at "Step 2"
|
| 143 |
+
fi
|
| 144 |
+
|
| 145 |
+
log " Found Dockerfile in $DOCKER_CONTEXT"
|
| 146 |
+
|
| 147 |
+
BUILD_OK=false
|
| 148 |
+
BUILD_OUTPUT=$(run_with_timeout "$DOCKER_BUILD_TIMEOUT" docker build "$DOCKER_CONTEXT" 2>&1) && BUILD_OK=true
|
| 149 |
+
|
| 150 |
+
if [ "$BUILD_OK" = true ]; then
|
| 151 |
+
pass "Docker build succeeded"
|
| 152 |
+
else
|
| 153 |
+
fail "Docker build failed (timeout=${DOCKER_BUILD_TIMEOUT}s)"
|
| 154 |
+
printf "%s\n" "$BUILD_OUTPUT" | tail -20
|
| 155 |
+
stop_at "Step 2"
|
| 156 |
+
fi
|
| 157 |
+
|
| 158 |
+
log "${BOLD}Step 3/3: Running openenv validate${NC} ..."
|
| 159 |
+
|
| 160 |
+
if ! command -v openenv &>/dev/null; then
|
| 161 |
+
fail "openenv command not found"
|
| 162 |
+
hint "Install it: pip install openenv-core"
|
| 163 |
+
stop_at "Step 3"
|
| 164 |
+
fi
|
| 165 |
+
|
| 166 |
+
VALIDATE_OK=false
|
| 167 |
+
VALIDATE_OUTPUT=$(cd "$REPO_DIR" && openenv validate 2>&1) && VALIDATE_OK=true
|
| 168 |
+
|
| 169 |
+
if [ "$VALIDATE_OK" = true ]; then
|
| 170 |
+
pass "openenv validate passed"
|
| 171 |
+
[ -n "$VALIDATE_OUTPUT" ] && log " $VALIDATE_OUTPUT"
|
| 172 |
+
else
|
| 173 |
+
fail "openenv validate failed"
|
| 174 |
+
printf "%s\n" "$VALIDATE_OUTPUT"
|
| 175 |
+
stop_at "Step 3"
|
| 176 |
+
fi
|
| 177 |
+
|
| 178 |
+
printf "\n"
|
| 179 |
+
printf "${BOLD}========================================${NC}\n"
|
| 180 |
+
printf "${GREEN}${BOLD} All 3/3 checks passed!${NC}\n"
|
| 181 |
+
printf "${GREEN}${BOLD} Your submission is ready to submit.${NC}\n"
|
| 182 |
+
printf "${BOLD}========================================${NC}\n"
|
| 183 |
+
printf "\n"
|
| 184 |
+
|
| 185 |
+
exit 0
|