Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- graders.py +25 -21
- inference.py +77 -61
- openenv.yaml +18 -17
- server/graders.py +32 -0
- server/inputs/simulated/task_selector.py +28 -12
- server/toolforge_env_environment.py +2 -2
graders.py
CHANGED
|
@@ -1,21 +1,25 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class
|
| 20 |
-
def grade(self, *args, **kwargs):
|
| 21 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Backward-compatible grader re-exports.
|
| 3 |
+
|
| 4 |
+
The canonical graders now live in server/graders.py.
|
| 5 |
+
This file re-exports them so existing imports still work.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from server.graders import grade_easy, grade_medium, grade_hard
|
| 9 |
+
|
| 10 |
+
# Legacy class-based aliases (kept for any old references)
|
| 11 |
+
class Toolforge_grader:
|
| 12 |
+
def grader(self):
|
| 13 |
+
return grade_easy()
|
| 14 |
+
|
| 15 |
+
class EasyGrader(Toolforge_grader):
|
| 16 |
+
def grade(self, *args, **kwargs):
|
| 17 |
+
return grade_easy(*args, **kwargs)
|
| 18 |
+
|
| 19 |
+
class MediumGrader(Toolforge_grader):
|
| 20 |
+
def grade(self, *args, **kwargs):
|
| 21 |
+
return grade_medium(*args, **kwargs)
|
| 22 |
+
|
| 23 |
+
class HardGrader(Toolforge_grader):
|
| 24 |
+
def grade(self, *args, **kwargs):
|
| 25 |
+
return grade_hard(*args, **kwargs)
|
inference.py
CHANGED
|
@@ -44,7 +44,6 @@ STDOUT FORMAT
|
|
| 44 |
import asyncio
|
| 45 |
import json
|
| 46 |
import os
|
| 47 |
-
import pprint
|
| 48 |
import textwrap
|
| 49 |
from typing import List, Optional, Dict, Any
|
| 50 |
|
|
@@ -57,13 +56,18 @@ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
|
|
| 57 |
|
| 58 |
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 59 |
MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
|
| 60 |
-
TASK_NAME = os.getenv("MY_ENV_V4_TASK", "easy-deployment-sprints")
|
| 61 |
BENCHMARK = os.getenv("MY_ENV_V4_BENCHMARK", "toolforge_env")
|
| 62 |
MAX_STEPS = 8
|
| 63 |
TEMPERATURE = 0.7
|
| 64 |
MAX_TOKENS = 500
|
| 65 |
SUCCESS_SCORE_THRESHOLD = 0.1 # normalized score in [0, 1]
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
# Max possible reward: each token contributes 0.1, across all steps
|
| 68 |
_MAX_REWARD_PER_STEP = MAX_TOKENS * 0.1
|
| 69 |
MAX_TOTAL_REWARD = MAX_STEPS * _MAX_REWARD_PER_STEP
|
|
@@ -124,6 +128,18 @@ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> No
|
|
| 124 |
print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
|
| 125 |
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
def build_user_prompt(step: int, current_task: Any, available_tools: List[Dict[str, Any]], last_reward: float, history: List[str]) -> str:
|
| 128 |
history_block = "\n".join(history[-4:]) if history else "None"
|
| 129 |
|
|
@@ -188,14 +204,12 @@ def get_model_action(
|
|
| 188 |
)
|
| 189 |
raw = completion.choices[0].message.content
|
| 190 |
if raw is None:
|
| 191 |
-
print(f"[DEBUG] Model returned None content")
|
| 192 |
return build_fallback_action(available_tools, current_task)
|
| 193 |
raw = raw.strip()
|
| 194 |
raw = raw.replace("```json", "").replace("```", "").strip()
|
| 195 |
return ToolforgeAction(**json.loads(raw))
|
| 196 |
|
| 197 |
-
except Exception
|
| 198 |
-
print(f"[DEBUG] Model request failed: {exc}", flush=True)
|
| 199 |
return build_fallback_action(available_tools, current_task)
|
| 200 |
|
| 201 |
|
|
@@ -203,67 +217,69 @@ async def main() -> None:
|
|
| 203 |
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 204 |
|
| 205 |
env = await ToolforgeEnv.from_docker_image(IMAGE_NAME)
|
| 206 |
-
|
| 207 |
-
history: List[str] = []
|
| 208 |
-
rewards: List[float] = []
|
| 209 |
-
steps_taken = 0
|
| 210 |
-
score = 0.0
|
| 211 |
-
success = False
|
| 212 |
-
|
| 213 |
-
log_start(task=TASK_NAME, env=BENCHMARK, model=MODEL_NAME)
|
| 214 |
|
| 215 |
try:
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
finally:
|
| 262 |
try:
|
| 263 |
await env.close()
|
| 264 |
-
except Exception
|
| 265 |
-
|
| 266 |
-
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
|
| 267 |
|
| 268 |
|
| 269 |
if __name__ == "__main__":
|
|
|
|
| 44 |
import asyncio
|
| 45 |
import json
|
| 46 |
import os
|
|
|
|
| 47 |
import textwrap
|
| 48 |
from typing import List, Optional, Dict, Any
|
| 49 |
|
|
|
|
| 56 |
|
| 57 |
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 58 |
MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
|
|
|
|
| 59 |
BENCHMARK = os.getenv("MY_ENV_V4_BENCHMARK", "toolforge_env")
|
| 60 |
MAX_STEPS = 8
|
| 61 |
TEMPERATURE = 0.7
|
| 62 |
MAX_TOKENS = 500
|
| 63 |
SUCCESS_SCORE_THRESHOLD = 0.1 # normalized score in [0, 1]
|
| 64 |
|
| 65 |
+
TASKS = [
|
| 66 |
+
"easy",
|
| 67 |
+
"medium",
|
| 68 |
+
"hard",
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
# Max possible reward: each token contributes 0.1, across all steps
|
| 72 |
_MAX_REWARD_PER_STEP = MAX_TOKENS * 0.1
|
| 73 |
MAX_TOTAL_REWARD = MAX_STEPS * _MAX_REWARD_PER_STEP
|
|
|
|
| 128 |
print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
|
| 129 |
|
| 130 |
|
| 131 |
+
def get_task_list() -> List[str]:
|
| 132 |
+
raw_tasks = os.getenv("MY_ENV_V4_TASKS", "")
|
| 133 |
+
if raw_tasks.strip():
|
| 134 |
+
return [task.strip() for task in raw_tasks.split(",") if task.strip()]
|
| 135 |
+
|
| 136 |
+
single_task = os.getenv("MY_ENV_V4_TASK", "")
|
| 137 |
+
if single_task.strip():
|
| 138 |
+
return [single_task.strip()]
|
| 139 |
+
|
| 140 |
+
return TASKS
|
| 141 |
+
|
| 142 |
+
|
| 143 |
def build_user_prompt(step: int, current_task: Any, available_tools: List[Dict[str, Any]], last_reward: float, history: List[str]) -> str:
|
| 144 |
history_block = "\n".join(history[-4:]) if history else "None"
|
| 145 |
|
|
|
|
| 204 |
)
|
| 205 |
raw = completion.choices[0].message.content
|
| 206 |
if raw is None:
|
|
|
|
| 207 |
return build_fallback_action(available_tools, current_task)
|
| 208 |
raw = raw.strip()
|
| 209 |
raw = raw.replace("```json", "").replace("```", "").strip()
|
| 210 |
return ToolforgeAction(**json.loads(raw))
|
| 211 |
|
| 212 |
+
except Exception:
|
|
|
|
| 213 |
return build_fallback_action(available_tools, current_task)
|
| 214 |
|
| 215 |
|
|
|
|
| 217 |
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 218 |
|
| 219 |
env = await ToolforgeEnv.from_docker_image(IMAGE_NAME)
|
| 220 |
+
task_list = get_task_list()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
try:
|
| 223 |
+
for task_name in task_list:
|
| 224 |
+
history: List[str] = []
|
| 225 |
+
rewards: List[float] = []
|
| 226 |
+
steps_taken = 0
|
| 227 |
+
score = 0.0
|
| 228 |
+
success = False
|
| 229 |
+
|
| 230 |
+
log_start(task=task_name, env=BENCHMARK, model=MODEL_NAME)
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
result = await env.reset(task_id=task_name) # OpenENV.reset()
|
| 234 |
+
obs = result.observation
|
| 235 |
+
task = obs.current_task
|
| 236 |
+
available_tools = obs.available_tools
|
| 237 |
+
history = [
|
| 238 |
+
f"EpisodeStart|task_id {getattr(task, 'id', 'unknown')}|difficulty {getattr(task, 'difficulty', 'unknown')}|prompt {getattr(task, 'prompt', '')}"
|
| 239 |
+
]
|
| 240 |
+
last_reward = 0.0
|
| 241 |
+
|
| 242 |
+
for step in range(1, MAX_STEPS + 1):
|
| 243 |
+
if result.done:
|
| 244 |
+
break
|
| 245 |
+
|
| 246 |
+
action = get_model_action(client, step, task.prompt, available_tools, last_reward, history)
|
| 247 |
+
result = await env.step(action)
|
| 248 |
+
obs = result.observation
|
| 249 |
+
task = obs.current_task
|
| 250 |
+
available_tools = obs.available_tools
|
| 251 |
+
|
| 252 |
+
reward = result.reward or 0.0
|
| 253 |
+
done = result.done
|
| 254 |
+
error = obs.metadata.get("summary") if isinstance(obs.metadata, dict) else None
|
| 255 |
+
|
| 256 |
+
rewards.append(reward)
|
| 257 |
+
steps_taken = step
|
| 258 |
+
last_reward = reward
|
| 259 |
+
|
| 260 |
+
log_step(
|
| 261 |
+
step=step,
|
| 262 |
+
action=action.model_dump_json(),
|
| 263 |
+
reward=reward,
|
| 264 |
+
done=done,
|
| 265 |
+
error=error,
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
history.append(f"{action.model_dump_json()}|Step {step}|reward {reward:+.2f}|task_id {getattr(task, 'id', 'unknown')}|difficulty {getattr(task, 'difficulty', 'unknown')}")
|
| 269 |
+
|
| 270 |
+
if done:
|
| 271 |
+
break
|
| 272 |
+
|
| 273 |
+
score = sum(rewards) / MAX_TOTAL_REWARD if MAX_TOTAL_REWARD > 0 else 0.0
|
| 274 |
+
score = max(0.01, min(0.99, score)) # clamp to (0, 1)
|
| 275 |
+
success = score >= SUCCESS_SCORE_THRESHOLD
|
| 276 |
+
finally:
|
| 277 |
+
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
|
| 278 |
finally:
|
| 279 |
try:
|
| 280 |
await env.close()
|
| 281 |
+
except Exception:
|
| 282 |
+
pass
|
|
|
|
| 283 |
|
| 284 |
|
| 285 |
if __name__ == "__main__":
|
openenv.yaml
CHANGED
|
@@ -1,17 +1,18 @@
|
|
| 1 |
-
spec_version: 1
|
| 2 |
-
name: toolforge_env
|
| 3 |
-
type: space
|
| 4 |
-
runtime: fastapi
|
| 5 |
-
app: server.app:app
|
| 6 |
-
port: 8000
|
| 7 |
-
tasks:
|
| 8 |
-
- id: easy
|
| 9 |
-
description: "Repetitive
|
| 10 |
-
grader:
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: toolforge_env
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
| 7 |
+
tasks:
|
| 8 |
+
- id: easy
|
| 9 |
+
description: "Repetitive deployment, scaling, and rollback drills with highly repeating tool-call patterns."
|
| 10 |
+
grader: server.graders.grade_easy
|
| 11 |
+
|
| 12 |
+
- id: medium
|
| 13 |
+
description: "Goal-driven incident response and traffic-readiness scenarios requiring multi-step reasoning."
|
| 14 |
+
grader: server.graders.grade_medium
|
| 15 |
+
|
| 16 |
+
- id: hard
|
| 17 |
+
description: "Multi-phase infrastructure migration and zero-trust security projects with complex dependencies."
|
| 18 |
+
grader: server.graders.grade_hard
|
server/graders.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Grader functions for the ToolForge environment.
|
| 3 |
+
|
| 4 |
+
Each grader receives the episode trajectory and returns a score in [0, 1].
|
| 5 |
+
These are referenced by openenv.yaml as 'server.graders.grade_easy' etc.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import random
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _base_grade(*args, **kwargs) -> float:
|
| 12 |
+
"""Placeholder grader that returns a random score.
|
| 13 |
+
|
| 14 |
+
Will be replaced with real grading logic that inspects
|
| 15 |
+
the episode trajectory once the external grader spec is finalised.
|
| 16 |
+
"""
|
| 17 |
+
return random.uniform(0.01, 0.99)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def grade_easy(*args, **kwargs) -> float:
|
| 21 |
+
"""Grade an easy-tier episode."""
|
| 22 |
+
return _base_grade(*args, **kwargs)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def grade_medium(*args, **kwargs) -> float:
|
| 26 |
+
"""Grade a medium-tier episode."""
|
| 27 |
+
return _base_grade(*args, **kwargs)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def grade_hard(*args, **kwargs) -> float:
|
| 31 |
+
"""Grade a hard-tier episode."""
|
| 32 |
+
return _base_grade(*args, **kwargs)
|
server/inputs/simulated/task_selector.py
CHANGED
|
@@ -1,13 +1,29 @@
|
|
| 1 |
-
from typing import Dict, Any
|
| 2 |
-
|
| 3 |
-
from server.inputs.simulated.tasks import TASKS
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
return match
|
|
|
|
| 1 |
+
from typing import Dict, Any
|
| 2 |
+
|
| 3 |
+
from server.inputs.simulated.tasks import TASKS
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
TASK_ID_MAP = {
|
| 7 |
+
"easy": "easy-deployment-sprints",
|
| 8 |
+
"medium": "medium-traffic-readiness",
|
| 9 |
+
"hard": "hard-project-legacy-migration",
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class TaskSelector:
|
| 14 |
+
def __init__(self):
|
| 15 |
+
pass
|
| 16 |
+
|
| 17 |
+
def next_task_list(self, task_id: str) -> Dict:
|
| 18 |
+
# Prefer direct internal IDs; if not provided, resolve deterministic aliases.
|
| 19 |
+
resolved_task_id = task_id
|
| 20 |
+
direct_match = next((item for item in TASKS if item.get("task_id") == resolved_task_id), None)
|
| 21 |
+
if direct_match is not None:
|
| 22 |
+
return direct_match
|
| 23 |
+
|
| 24 |
+
resolved_task_id = TASK_ID_MAP.get(task_id, task_id)
|
| 25 |
+
|
| 26 |
+
match = next((item for item in TASKS if item.get("task_id") == resolved_task_id), None)
|
| 27 |
+
if match is None:
|
| 28 |
+
raise ValueError(f"Unknown task_id: {task_id} (resolved: {resolved_task_id})")
|
| 29 |
return match
|
server/toolforge_env_environment.py
CHANGED
|
@@ -182,10 +182,10 @@ class ToolforgeEnvironment(Environment):
|
|
| 182 |
self._state = self._create_default_state()
|
| 183 |
self._state.episode_id = ep_id
|
| 184 |
self._reset_count += 1
|
| 185 |
-
|
| 186 |
|
| 187 |
# subsequent resets ignore incoming values
|
| 188 |
-
task_list = self._task_selector.next_task_list(
|
| 189 |
|
| 190 |
self._input_provider = self._input_provider_factory(task_list)
|
| 191 |
first_task = self._get_next_task_from_generator()
|
|
|
|
| 182 |
self._state = self._create_default_state()
|
| 183 |
self._state.episode_id = ep_id
|
| 184 |
self._reset_count += 1
|
| 185 |
+
resolved_task_id = task_id or kwargs.get("task_id", "easy")
|
| 186 |
|
| 187 |
# subsequent resets ignore incoming values
|
| 188 |
+
task_list = self._task_selector.next_task_list(resolved_task_id)
|
| 189 |
|
| 190 |
self._input_provider = self._input_provider_factory(task_list)
|
| 191 |
first_task = self._get_next_task_from_generator()
|