rudhratej09 commited on
Commit
5aadfb7
·
1 Parent(s): 0f6a469

updates using dip's prompt made the class directly callable

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. server/env.py +1 -1
  3. server/graders.py +41 -120
README.md CHANGED
@@ -164,7 +164,7 @@ python inference.py
164
  ```
165
  [START] task=task_1 env=eco_llm_inference_routing model=Qwen/Qwen2.5-72B-Instruct
166
  [STEP] step=1 action=strategy=NONE,model=MEDIUM,exit=false reward=0.68 done=true error=null
167
- [END] success=true steps=1 rewards=0.68
168
  ```
169
 
170
  ---
 
164
  ```
165
  [START] task=task_1 env=eco_llm_inference_routing model=Qwen/Qwen2.5-72B-Instruct
166
  [STEP] step=1 action=strategy=NONE,model=MEDIUM,exit=false reward=0.68 done=true error=null
167
+ [END] success=true steps=1 score=0.45 rewards=0.68
168
  ```
169
 
170
  ---
server/env.py CHANGED
@@ -227,7 +227,7 @@ class EcoLLMInferenceRoutingEnvironment(Environment[RLAction, RLObservation, RLS
227
  reward=reward.total_reward,
228
  reward_details=reward,
229
  kb_available=query.kb_available,
230
- score=float(self._state.query_index) / float(len(self.current_task.queries)) if done else 0.0,
231
  metadata={
232
  "task_id": self.current_task.task_id,
233
  "difficulty": self.current_task.difficulty,
 
227
  reward=reward.total_reward,
228
  reward_details=reward,
229
  kb_available=query.kb_available,
230
+ score=0.0,
231
  metadata={
232
  "task_id": self.current_task.task_id,
233
  "difficulty": self.current_task.difficulty,
server/graders.py CHANGED
@@ -1,175 +1,96 @@
1
- """
2
- server/graders.py
3
- =================
4
- Grader CLASSES for the Eco-LLM Inference Routing tasks.
5
-
6
- WHY CLASSES (not functions)
7
- ────────────────────────────
8
- The OpenEnv Phase 2 validator resolves the grader field in openenv.yaml by:
9
- 1. Splitting "server.graders:Task1Grader" on ":"
10
- 2. importlib.import_module("server.graders")
11
- 3. getattr(module, "Task1Grader")
12
- 4. instance = Task1Grader() ← instantiates the class
13
- 5. result = instance(episode_data) ← calls it with episode data
14
-
15
- A plain function reference like "server.app:grade_episode" fails at step 4
16
- because grade_episode() requires positional arguments and cannot be called
17
- as grade_episode() with no args to produce an instance.
18
-
19
- WHY SCORES IN [0.01, 0.99]
20
- ────────────────────────────
21
- The Phase 2 validator applies a strict inequality check: 0.0 < score < 1.0.
22
- Returning exactly 0.0 or 1.0 triggers "Score Out of Range" and fails the task.
23
- Community-verified fix (April 2026): clamp to [0.01, 0.99].
24
-
25
- GRADING FORMULA
26
- ────────────────
27
- score = clamp(total_reward / max_possible_reward, 0.01, 0.99)
28
-
29
- task_1 → max = 1 query × 1.5 = 1.5
30
- task_2 → max = 3 queries × 1.5 = 4.5
31
- task_3 → max = 5 queries × 1.5 = 7.5
32
-
33
- The grader accepts any of these call signatures:
34
- grader(episode_data_dict)
35
- grader.grade(episode_data_dict)
36
- grader.score(episode_data_dict)
37
- """
38
 
39
  from __future__ import annotations
40
 
41
  from typing import Any
42
 
43
- # ── Constants ─────────────────────────────────────────────────────────────────
44
-
45
- # Maximum achievable reward per query:
46
- # score(1.0) + cache_bonus(0.5) − 0 penalties = 1.5
47
  _MAX_REWARD_PER_QUERY: float = 1.5
48
 
49
  _MAX_REWARDS: dict[str, float] = {
50
- "task_1": 1.0 * _MAX_REWARD_PER_QUERY, # 1.5
51
- "task_2": 3.0 * _MAX_REWARD_PER_QUERY, # 4.5
52
- "task_3": 5.0 * _MAX_REWARD_PER_QUERY, # 7.5
53
  }
54
 
55
- # Phase 2 validator requires STRICT inequality: 0.0 < score < 1.0
56
- # Clamp to [0.01, 0.99] to guarantee this regardless of episode outcome.
57
  _SCORE_MIN: float = 0.01
58
  _SCORE_MAX: float = 0.99
59
 
60
 
61
  def _compute_score(task_id: str, rewards: list[float]) -> float:
62
- """Normalise total reward to [0.01, 0.99]."""
63
- max_r = _MAX_REWARDS.get(task_id, _MAX_REWARDS["task_1"])
64
- total = sum(rewards)
65
- raw = total / max_r if max_r > 0 else 0.0
66
- return float(max(_SCORE_MIN, min(_SCORE_MAX, raw)))
67
 
68
 
69
  def _extract_rewards(episode_data: dict[str, Any]) -> list[float]:
70
- """Pull per-step rewards out of whatever the validator sends."""
71
- rewards = episode_data.get("rewards") or episode_data.get("per_step_rewards") or []
72
- return [float(r) for r in rewards]
 
 
 
73
 
74
 
75
  def _make_result(task_id: str, score: float, rewards: list[float]) -> dict[str, Any]:
76
  return {
77
- "score": round(score, 4),
78
- "success": score >= 0.5,
79
- "task_id": task_id,
80
  "total_reward": round(sum(rewards), 4),
81
- "steps": len(rewards),
82
  "details": {
83
- "per_step_rewards": [round(r, 4) for r in rewards],
84
- "max_possible_reward": _MAX_REWARDS.get(task_id, 1.5),
85
- "score_range": [_SCORE_MIN, _SCORE_MAX],
86
  },
87
  }
88
 
89
 
90
- # ── Grader Classes ────────────────────────────────────────────────────────────
91
- # Each class:
92
- # - Is instantiable with no arguments: Task1Grader()
93
- # - Is callable: instance(episode_data) → result dict
94
- # - Exposes .grade() and .score() as aliases
95
- # ──────────────────────────────────────────────────────────────────────────────
96
 
97
- class Task1Grader:
98
- """
99
- Grader for task_1 — Single Query Routing (Easy).
 
 
100
 
101
- Score = clamp(total_reward / 1.5, 0.01, 0.99)
102
- """
103
 
 
104
  task_id: str = "task_1"
105
 
106
  def __call__(self, episode_data: dict[str, Any]) -> dict[str, Any]:
107
  rewards = _extract_rewards(episode_data)
108
- score = _compute_score(self.task_id, rewards)
109
- return _make_result(self.task_id, score, rewards)
110
-
111
- def grade(self, episode_data: dict[str, Any]) -> dict[str, Any]:
112
- return self(episode_data)
113
-
114
- def score(self, episode_data: dict[str, Any]) -> float:
115
- return self(episode_data)["score"]
116
-
117
-
118
- class Task2Grader:
119
- """
120
- Grader for task_2 — Multi-Query Episode (Medium).
121
-
122
- Score = clamp(total_reward / 4.5, 0.01, 0.99)
123
- Note: LARGE model incurs -0.2 penalty per use, making 4.5 the ceiling
124
- only when LARGE is never used and all 3 queries are cache-hit.
125
- """
126
-
127
- task_id: str = "task_2"
128
-
129
- def __call__(self, episode_data: dict[str, Any]) -> dict[str, Any]:
130
- rewards = _extract_rewards(episode_data)
131
- score = _compute_score(self.task_id, rewards)
132
  return _make_result(self.task_id, score, rewards)
133
 
134
  def grade(self, episode_data: dict[str, Any]) -> dict[str, Any]:
135
  return self(episode_data)
136
 
137
  def score(self, episode_data: dict[str, Any]) -> float:
138
- return self(episode_data)["score"]
139
 
140
 
141
- class Task3Grader:
142
- """
143
- Grader for task_3 — Stateful Carbon-Aware Routing (Hard).
144
 
145
- Score = clamp(total_reward / 7.5, 0.01, 0.99)
146
- 5-query episode with caching, KB lookups, and carbon-aware waiting.
147
- """
148
 
149
- task_id: str = "task_3"
 
150
 
151
- def __call__(self, episode_data: dict[str, Any]) -> dict[str, Any]:
152
- rewards = _extract_rewards(episode_data)
153
- score = _compute_score(self.task_id, rewards)
154
- return _make_result(self.task_id, score, rewards)
155
-
156
- def grade(self, episode_data: dict[str, Any]) -> dict[str, Any]:
157
- return self(episode_data)
158
-
159
- def score(self, episode_data: dict[str, Any]) -> float:
160
- return self(episode_data)["score"]
161
 
 
 
162
 
163
- # ── Registry (convenience) ────────────────────────────────────────────────────
164
 
165
- GRADERS: dict[str, type] = {
166
  "task_1": Task1Grader,
167
  "task_2": Task2Grader,
168
  "task_3": Task3Grader,
169
  }
170
 
171
 
172
- def get_grader(task_id: str) -> Task1Grader | Task2Grader | Task3Grader:
173
- """Return an instantiated grader for the given task_id."""
174
- cls = GRADERS.get(task_id, Task1Grader)
175
- return cls()
 
1
+ """Compatibility graders for OpenEnv hackathon validators."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  from __future__ import annotations
4
 
5
  from typing import Any
6
 
 
 
 
 
7
  _MAX_REWARD_PER_QUERY: float = 1.5
8
 
9
  _MAX_REWARDS: dict[str, float] = {
10
+ "task_1": 1.0 * _MAX_REWARD_PER_QUERY,
11
+ "task_2": 3.0 * _MAX_REWARD_PER_QUERY,
12
+ "task_3": 5.0 * _MAX_REWARD_PER_QUERY,
13
  }
14
 
 
 
15
  _SCORE_MIN: float = 0.01
16
  _SCORE_MAX: float = 0.99
17
 
18
 
19
  def _compute_score(task_id: str, rewards: list[float]) -> float:
20
+ max_reward = _MAX_REWARDS.get(task_id, _MAX_REWARDS["task_1"])
21
+ total_reward = sum(rewards)
22
+ raw_score = total_reward / max_reward if max_reward > 0 else 0.0
23
+ return float(max(_SCORE_MIN, min(_SCORE_MAX, raw_score)))
 
24
 
25
 
26
  def _extract_rewards(episode_data: dict[str, Any]) -> list[float]:
27
+ rewards = episode_data.get("rewards")
28
+ if rewards is None:
29
+ rewards = episode_data.get("per_step_rewards")
30
+ if rewards is None:
31
+ rewards = []
32
+ return [float(reward) for reward in rewards]
33
 
34
 
35
  def _make_result(task_id: str, score: float, rewards: list[float]) -> dict[str, Any]:
36
  return {
37
+ "score": round(score, 4),
38
+ "success": score >= 0.5,
39
+ "task_id": task_id,
40
  "total_reward": round(sum(rewards), 4),
41
+ "steps": len(rewards),
42
  "details": {
43
+ "per_step_rewards": [round(reward, 4) for reward in rewards],
44
+ "max_possible_reward": _MAX_REWARDS.get(task_id, _MAX_REWARDS["task_1"]),
45
+ "score_range": [_SCORE_MIN, _SCORE_MAX],
46
  },
47
  }
48
 
49
 
50
+ class _GraderMeta(type):
51
+ """Allow both Task1Grader() and Task1Grader(episode_data)."""
 
 
 
 
52
 
53
+ def __call__(cls, *args: Any, **kwargs: Any) -> Any:
54
+ if args and isinstance(args[0], dict) and len(args) == 1 and not kwargs:
55
+ instance = super().__call__()
56
+ return instance(args[0])
57
+ return super().__call__(*args, **kwargs)
58
 
 
 
59
 
60
+ class BaseGrader(metaclass=_GraderMeta):
61
  task_id: str = "task_1"
62
 
63
  def __call__(self, episode_data: dict[str, Any]) -> dict[str, Any]:
64
  rewards = _extract_rewards(episode_data)
65
+ score = _compute_score(self.task_id, rewards)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  return _make_result(self.task_id, score, rewards)
67
 
68
  def grade(self, episode_data: dict[str, Any]) -> dict[str, Any]:
69
  return self(episode_data)
70
 
71
  def score(self, episode_data: dict[str, Any]) -> float:
72
+ return float(self(episode_data)["score"])
73
 
74
 
75
+ class Task1Grader(BaseGrader):
76
+ task_id = "task_1"
 
77
 
 
 
 
78
 
79
+ class Task2Grader(BaseGrader):
80
+ task_id = "task_2"
81
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ class Task3Grader(BaseGrader):
84
+ task_id = "task_3"
85
 
 
86
 
87
+ GRADERS: dict[str, type[BaseGrader]] = {
88
  "task_1": Task1Grader,
89
  "task_2": Task2Grader,
90
  "task_3": Task3Grader,
91
  }
92
 
93
 
94
+ def get_grader(task_id: str) -> BaseGrader:
95
+ grader_cls = GRADERS.get(task_id, Task1Grader)
96
+ return grader_cls()