Hugging-coder14 commited on
Commit
2b3313a
·
verified ·
1 Parent(s): 36205b4

Upload folder using huggingface_hub

Browse files
graders.py CHANGED
@@ -1,25 +1,15 @@
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)
 
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, EpisodeGrader
9
+
10
+ __all__ = [
11
+ "grade_easy",
12
+ "grade_medium",
13
+ "grade_hard",
14
+ "EpisodeGrader",
15
+ ]
 
 
 
 
 
 
 
 
 
 
models.py CHANGED
@@ -135,6 +135,42 @@ class ToolforgeObservation(Observation):
135
  )
136
 
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  class ToolForgeState(State):
139
  """
140
  The internal State class for the ToolForge environment.
@@ -177,6 +213,9 @@ class ToolForgeState(State):
177
  # Macro name -> ordered atomic tool names it represents
178
  macro_definitions: Dict[str, List[str]] = Field(default_factory=dict)
179
 
 
 
 
180
 
181
  class PlanAccuracyResult(BaseModel):
182
  """Output of the Stage-3 plan accuracy calculator."""
@@ -288,6 +327,14 @@ class PipelineResult(BaseModel):
288
  # Human-readable one-line summary
289
  summary: str
290
 
 
 
 
 
 
 
 
 
291
 
292
 
293
 
 
135
  )
136
 
137
 
138
+ class EpisodeGradingState(BaseModel):
139
+ """Aggregate episode-level signals consumed by the grader.
140
+
141
+ Accumulated incrementally during the episode by the environment.
142
+ Read by the grader at episode end to compute a normalized score.
143
+ """
144
+ # Total steps taken in this episode
145
+ episode_steps: int = 0
146
+ # Steps where structural validation failed
147
+ validation_failures: int = 0
148
+ # Steps where harmful tool calls were detected
149
+ harmful_plan_count: int = 0
150
+ # Steps with full slot completion AND passed validation
151
+ correct_plan_count: int = 0
152
+ # Steps where efficiency was computed (slot_ratio == 1.0)
153
+ fully_correct_efficiency_opportunities: int = 0
154
+ # Sum of efficiency scores across fully-correct steps
155
+ sum_efficiency_score: float = 0.0
156
+ # Times the agent attempted to create a macro
157
+ macro_creation_attempts: int = 0
158
+ # Times macro creation was approved by the environment
159
+ macro_creation_approved: int = 0
160
+ # Approved AND plan was semantically valid
161
+ macro_creation_correct: int = 0
162
+ # Sum of macro creation bonuses awarded
163
+ macro_creation_bonus_total: float = 0.0
164
+ # Steps where any accepted macro was used in the plan
165
+ macro_usage_attempts: int = 0
166
+ # Macro used AND slot_ratio >= threshold
167
+ macro_usage_correct: int = 0
168
+ # Times macro creation was rejected
169
+ macro_rejected_count: int = 0
170
+ # Total tasks completed by episode end
171
+ final_completed_tasks: int = 0
172
+
173
+
174
  class ToolForgeState(State):
175
  """
176
  The internal State class for the ToolForge environment.
 
213
  # Macro name -> ordered atomic tool names it represents
214
  macro_definitions: Dict[str, List[str]] = Field(default_factory=dict)
215
 
216
+ # Episode-level grading accumulator (reset each episode)
217
+ grading: EpisodeGradingState = Field(default_factory=EpisodeGradingState)
218
+
219
 
220
  class PlanAccuracyResult(BaseModel):
221
  """Output of the Stage-3 plan accuracy calculator."""
 
327
  # Human-readable one-line summary
328
  summary: str
329
 
330
+ # --- Grader-facing step facts (set by pipeline, consumed by environment) ---
331
+ step_slot_ratio: Optional[float] = None
332
+ step_task_complete: Optional[bool] = None
333
+ step_harmful: Optional[bool] = None
334
+ step_macro_creation_bonus: Optional[float] = None
335
+ step_macro_usage_bonus: Optional[float] = None
336
+ step_efficiency_score: Optional[float] = None
337
+
338
 
339
 
340
 
openenv.yaml CHANGED
@@ -1,18 +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 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
 
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/evaluation_pipeline.py CHANGED
@@ -104,6 +104,12 @@ def run_evaluation_pipeline(
104
  reward=VALIDATION_PENALTY,
105
  passed_validation=False,
106
  summary=f"Validation failed: {validation.reason}",
 
 
 
 
 
 
107
  )
108
 
109
  # 2. run_slot_judgment(...)
@@ -133,6 +139,12 @@ def run_evaluation_pipeline(
133
  reward=FINAL_REWARD_MIN,
134
  passed_validation=True,
135
  summary="Harmful tool call detected.",
 
 
 
 
 
 
136
  )
137
 
138
  # 3. Compute step reward (slot score + macro bonuses + efficiency)
@@ -183,4 +195,10 @@ def run_evaluation_pipeline(
183
  reward=final_reward,
184
  passed_validation=True,
185
  summary="Plan validated, slots judged, final reward applied.",
 
 
 
 
 
 
186
  )
 
104
  reward=VALIDATION_PENALTY,
105
  passed_validation=False,
106
  summary=f"Validation failed: {validation.reason}",
107
+ step_slot_ratio=0.0,
108
+ step_task_complete=False,
109
+ step_harmful=False,
110
+ step_macro_creation_bonus=0.0,
111
+ step_macro_usage_bonus=0.0,
112
+ step_efficiency_score=0.0,
113
  )
114
 
115
  # 2. run_slot_judgment(...)
 
139
  reward=FINAL_REWARD_MIN,
140
  passed_validation=True,
141
  summary="Harmful tool call detected.",
142
+ step_slot_ratio=0.0,
143
+ step_task_complete=False,
144
+ step_harmful=True,
145
+ step_macro_creation_bonus=0.0,
146
+ step_macro_usage_bonus=0.0,
147
+ step_efficiency_score=0.0,
148
  )
149
 
150
  # 3. Compute step reward (slot score + macro bonuses + efficiency)
 
195
  reward=final_reward,
196
  passed_validation=True,
197
  summary="Plan validated, slots judged, final reward applied.",
198
+ step_slot_ratio=slot_ratio,
199
+ step_task_complete=slot_judgment.task_complete,
200
+ step_harmful=False,
201
+ step_macro_creation_bonus=macro_creation,
202
+ step_macro_usage_bonus=macro_usage,
203
+ step_efficiency_score=efficiency_score,
204
  )
server/graders.py CHANGED
@@ -1,32 +1,116 @@
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Grader functions for the ToolForge environment.
3
+
4
+ Each grader receives the complete environment state object and returns
5
+ a benchmark score in [0.01, 0.99]. All three difficulty levels use the
6
+ same EpisodeGrader class — difficulty differences are reflected by the
7
+ episode data itself, not by separate grading logic.
8
+
9
+ Referenced by openenv.yaml as 'server.graders.grade_easy' etc.
10
+ """
11
+
12
+ import logging
13
+ from typing import Any
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class EpisodeGrader:
19
+ """Reads the episode grading state and computes a normalized [0.01, 0.99] score.
20
+
21
+ Sub-scores:
22
+ accuracy — correct_plan_count / episode_steps
23
+ token_optimization — avg efficiency over fully-correct opportunities
24
+ macro_creation — macro_creation_correct / macro_creation_attempts
25
+ macro_usage — macro_usage_correct / macro_usage_attempts
26
+
27
+ Weights (accuracy > token_opt > macro_creation > macro_usage):
28
+ 0.40 / 0.30 / 0.20 / 0.10
29
+
30
+ Final score clamped to [0.01, 0.99].
31
+ """
32
+
33
+ WEIGHTS = {
34
+ "accuracy": 0.40,
35
+ "token_optimization": 0.30,
36
+ "macro_creation": 0.20,
37
+ "macro_usage": 0.10,
38
+ }
39
+
40
+ def grade(self, state: Any) -> float:
41
+ """Compute normalized episode score from the environment state.
42
+
43
+ Args:
44
+ state: The ToolForgeState object for the completed episode.
45
+
46
+ Returns:
47
+ Score clamped to [0.01, 0.99].
48
+ """
49
+ # Extract the grading accumulator from state
50
+ g = getattr(state, "grading", None)
51
+ if g is None:
52
+ logger.warning("EpisodeGrader: no grading state found, returning 0.01")
53
+ return 0.01
54
+
55
+ # --- Sub-score: accuracy ---
56
+ if g.episode_steps > 0:
57
+ accuracy = g.correct_plan_count / g.episode_steps
58
+ else:
59
+ accuracy = 0.0
60
+
61
+ # --- Sub-score: token optimization ---
62
+ if g.fully_correct_efficiency_opportunities > 0:
63
+ token_opt = g.sum_efficiency_score / g.fully_correct_efficiency_opportunities
64
+ else:
65
+ token_opt = 0.0
66
+
67
+ # --- Sub-score: macro creation ---
68
+ if g.macro_creation_attempts > 0:
69
+ macro_create = g.macro_creation_correct / g.macro_creation_attempts
70
+ else:
71
+ macro_create = 0.0
72
+
73
+ # --- Sub-score: macro usage ---
74
+ if g.macro_usage_attempts > 0:
75
+ macro_use = g.macro_usage_correct / g.macro_usage_attempts
76
+ else:
77
+ macro_use = 0.0
78
+
79
+ raw = (
80
+ self.WEIGHTS["accuracy"] * accuracy
81
+ + self.WEIGHTS["token_optimization"] * token_opt
82
+ + self.WEIGHTS["macro_creation"] * macro_create
83
+ + self.WEIGHTS["macro_usage"] * macro_use
84
+ )
85
+
86
+ final = max(0.01, min(0.99, raw))
87
+
88
+ logger.info(
89
+ "EpisodeGrader | accuracy=%.3f token_opt=%.3f macro_create=%.3f macro_use=%.3f | raw=%.4f final=%.4f",
90
+ accuracy, token_opt, macro_create, macro_use, raw, final,
91
+ )
92
+
93
+ return final
94
+
95
+
96
+ # Shared grader instance
97
+ _grader = EpisodeGrader()
98
+
99
+
100
+ def grade_easy(*args, **kwargs) -> float:
101
+ """Grade an easy-tier episode."""
102
+ # The framework passes the state as the first positional argument
103
+ state = args[0] if args else kwargs.get("state")
104
+ return _grader.grade(state)
105
+
106
+
107
+ def grade_medium(*args, **kwargs) -> float:
108
+ """Grade a medium-tier episode."""
109
+ state = args[0] if args else kwargs.get("state")
110
+ return _grader.grade(state)
111
+
112
+
113
+ def grade_hard(*args, **kwargs) -> float:
114
+ """Grade a hard-tier episode."""
115
+ state = args[0] if args else kwargs.get("state")
116
+ return _grader.grade(state)
server/inputs/simulated/task_selector.py CHANGED
@@ -1,29 +1,29 @@
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
 
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
@@ -35,6 +35,7 @@ try:
35
  ToolforgeObservation,
36
  ToolForgeState,
37
  Task,
 
38
  )
39
  except ImportError:
40
  from models import (
@@ -44,6 +45,7 @@ except ImportError:
44
  ToolforgeObservation,
45
  ToolForgeState,
46
  Task,
 
47
  )
48
 
49
  try:
@@ -204,6 +206,9 @@ class ToolforgeEnvironment(Environment):
204
  self._state.macro_definitions = prev_macro_defs
205
  self._last_approval = None
206
 
 
 
 
207
 
208
  obs = self._create_default_observation()
209
  obs.done = False
@@ -239,6 +244,10 @@ class ToolforgeEnvironment(Environment):
239
  self._state.step_count += 1
240
  self._last_approval = False
241
 
 
 
 
 
242
  progression = "advanced_to_next_task_malformed_action"
243
  if self._state.step_count >= self.MAX_EPISODE_STEPS:
244
  self._state.done = True
@@ -312,6 +321,9 @@ class ToolforgeEnvironment(Environment):
312
  if macro_result["decision"] == "approved" and action.macro_proposal is not None:
313
  self._state.available_tools.append(action.macro_proposal)
314
 
 
 
 
315
  # Fetch the scalar reward from the evaluation pipeline
316
  reward = float(pipeline_result.reward)
317
  print(self._state.current_task.prompt, reward, progression, macro_result)
@@ -350,6 +362,56 @@ class ToolforgeEnvironment(Environment):
350
  logger.debug("Next task prompt: %s", next_task.prompt)
351
  return True
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  def _analyze_plan(self, plan: List[ToolCall]) -> Dict[str, Any]:
354
  """Compute deterministic call accounting from server-side tool registry."""
355
 
 
35
  ToolforgeObservation,
36
  ToolForgeState,
37
  Task,
38
+ EpisodeGradingState,
39
  )
40
  except ImportError:
41
  from models import (
 
45
  ToolforgeObservation,
46
  ToolForgeState,
47
  Task,
48
+ EpisodeGradingState,
49
  )
50
 
51
  try:
 
206
  self._state.macro_definitions = prev_macro_defs
207
  self._last_approval = None
208
 
209
+ # Fresh episode-level grading accumulator
210
+ self._state.grading = EpisodeGradingState()
211
+
212
 
213
  obs = self._create_default_observation()
214
  obs.done = False
 
244
  self._state.step_count += 1
245
  self._last_approval = False
246
 
247
+ # Track malformed actions in grading state
248
+ self._state.grading.episode_steps += 1
249
+ self._state.grading.validation_failures += 1
250
+
251
  progression = "advanced_to_next_task_malformed_action"
252
  if self._state.step_count >= self.MAX_EPISODE_STEPS:
253
  self._state.done = True
 
321
  if macro_result["decision"] == "approved" and action.macro_proposal is not None:
322
  self._state.available_tools.append(action.macro_proposal)
323
 
324
+ # Accumulate grading state from this step
325
+ self._update_grading_state(pipeline_result, macro_result, action)
326
+
327
  # Fetch the scalar reward from the evaluation pipeline
328
  reward = float(pipeline_result.reward)
329
  print(self._state.current_task.prompt, reward, progression, macro_result)
 
362
  logger.debug("Next task prompt: %s", next_task.prompt)
363
  return True
364
 
365
+ def _update_grading_state(
366
+ self,
367
+ pipeline_result,
368
+ macro_result: Dict[str, Any],
369
+ action: ToolforgeAction,
370
+ ) -> None:
371
+ """Accumulate episode-level grading counters from a single step."""
372
+ g = self._state.grading
373
+ g.episode_steps += 1
374
+
375
+ # Validation failures
376
+ if not pipeline_result.passed_validation:
377
+ g.validation_failures += 1
378
+
379
+ # Harmful calls
380
+ if pipeline_result.step_harmful:
381
+ g.harmful_plan_count += 1
382
+
383
+ # Correct plans (full slot fill + valid)
384
+ if pipeline_result.step_task_complete and pipeline_result.passed_validation:
385
+ g.correct_plan_count += 1
386
+
387
+ # Efficiency tracking (only when slot_ratio == 1.0)
388
+ sr = pipeline_result.step_slot_ratio
389
+ if sr is not None and sr >= 1.0:
390
+ g.fully_correct_efficiency_opportunities += 1
391
+ g.sum_efficiency_score += (pipeline_result.step_efficiency_score or 0.0)
392
+
393
+ # Macro creation tracking
394
+ if macro_result["attempted"]:
395
+ g.macro_creation_attempts += 1
396
+ if macro_result["decision"] == "approved":
397
+ g.macro_creation_approved += 1
398
+ if pipeline_result.passed_validation:
399
+ g.macro_creation_correct += 1
400
+ g.macro_creation_bonus_total += (pipeline_result.step_macro_creation_bonus or 0.0)
401
+ elif macro_result["decision"] == "rejected":
402
+ g.macro_rejected_count += 1
403
+
404
+ # Macro usage tracking
405
+ macro_names = {m.name for m in self._state.accepted_macros}
406
+ if any(c.tool_name in macro_names for c in action.plan):
407
+ g.macro_usage_attempts += 1
408
+ if sr is not None and sr >= 0.65:
409
+ g.macro_usage_correct += 1
410
+
411
+ # Keep completed task count current
412
+ g.final_completed_tasks = len(self._state.completed_tasks)
413
+
414
+
415
  def _analyze_plan(self, plan: List[ToolCall]) -> Dict[str, Any]:
416
  """Compute deterministic call accounting from server-side tool registry."""
417