OpenEnv Agent commited on
Commit
cd5a326
·
1 Parent(s): c3e6ffa

fix: scores strictly between 0 and 1 exclusive (0.01-0.99), no exact boundaries

Browse files
Files changed (3) hide show
  1. app/environment.py +28 -42
  2. app/models.py +7 -7
  3. app/tasks.py +35 -73
app/environment.py CHANGED
@@ -19,13 +19,12 @@ class IncidentResponseEnv:
19
  def _reset_state(self):
20
  self.step_count = 0
21
  self.done = False
22
- self.cumulative_score = 0.0
23
  self.history = []
24
  self.grader_state: Dict[str, Any] = {
25
  "investigated_root_cause": False,
26
  "correct_escalation": False,
27
  "correct_fix_applied": False,
28
- # medium/hard extras
29
  "identified_cascade_origin": False,
30
  "identified_root_cause": False,
31
  "investigated_config_service": False,
@@ -41,7 +40,6 @@ class IncidentResponseEnv:
41
  def state(self) -> StateModel:
42
  grader_fn = GRADERS[self.task_id]
43
  score, _ = grader_fn(self.grader_state)
44
- score = max(0.01, min(0.99, score))
45
  return StateModel(
46
  task_id=self.task_id,
47
  step=self.step_count,
@@ -55,42 +53,40 @@ class IncidentResponseEnv:
55
  def step(self, action: Action) -> StepResult:
56
  if self.done:
57
  obs = self._build_observation("Episode already finished.")
58
- return StepResult(observation=obs, reward=Reward(value=0.0, reason="already done"), done=True, info={})
 
 
 
 
 
59
 
60
  self.step_count += 1
61
  reward_value, reward_reason = self._process_action(action)
62
 
63
- # penalize no_op
64
  if action.action_type == ActionType.NO_OP:
65
  self.grader_state["no_op_count"] += 1
66
- reward_value = -0.1
67
  reward_reason = "no_op penalty"
68
 
69
- # penalize excessive no_ops
70
  if self.grader_state["no_op_count"] >= 3:
71
- reward_value = -0.2
72
  reward_reason = "repeated no_op — agent appears stuck"
73
 
74
  grader_fn = GRADERS[self.task_id]
75
  current_score, _ = grader_fn(self.grader_state)
76
- current_score = max(0.01, min(0.99, current_score))
77
 
78
- # check done conditions
79
  max_steps_reached = self.step_count >= self.meta["max_steps"]
80
  task_complete = self._is_task_complete()
81
 
82
  if task_complete:
83
  self.done = True
84
- reward_value = max(reward_value, 0.2)
85
- reward_reason += " | task complete bonus"
86
 
87
  if max_steps_reached and not self.done:
88
  self.done = True
89
- reward_value = min(reward_value, -0.05)
90
  reward_reason += " | max steps reached"
91
 
92
  self.cumulative_score = current_score
93
-
94
  self.history.append({
95
  "step": self.step_count,
96
  "action": action.model_dump(),
@@ -113,44 +109,37 @@ class IncidentResponseEnv:
113
 
114
  if atype == ActionType.INVESTIGATE:
115
  return self._handle_investigate(target)
116
-
117
  elif atype == ActionType.ESCALATE:
118
  correct = scenario["correct_escalation"]
119
  if target == correct:
120
  if not self.grader_state["correct_escalation"]:
121
  self.grader_state["correct_escalation"] = True
122
- return 0.3, f"correct escalation to {target}"
123
- return 0.0, "already escalated correctly"
124
- else:
125
- return -0.1, f"wrong escalation target: {target}"
126
-
127
  elif atype == ActionType.APPLY_FIX:
128
  correct = scenario["correct_fix"]
129
  if target == correct:
130
  if not self.grader_state["correct_fix_applied"]:
131
  self.grader_state["correct_fix_applied"] = True
132
- return 0.4, f"correct fix applied: {target}"
133
- return 0.0, "fix already applied"
134
- else:
135
- return -0.15, f"wrong fix applied: {target} — may worsen incident"
136
-
137
  elif atype == ActionType.POSTMORTEM:
138
  return self._handle_postmortem(action.details or "")
139
-
140
- return 0.0, "unknown action"
141
 
142
  def _handle_investigate(self, target: str) -> Tuple[float, str]:
143
  scenario = self.scenario
144
  root_cause = scenario["root_cause"]
145
 
146
- # task-specific investigation tracking
147
  if self.task_id == "task_hard":
148
  if target == "config-service" and not self.grader_state["investigated_config_service"]:
149
  self.grader_state["investigated_config_service"] = True
150
- return 0.15, "investigated config-service — found partial secret rotation failure"
151
  if target == "auth-service" and not self.grader_state["investigated_auth_service"]:
152
  self.grader_state["investigated_auth_service"] = True
153
- return 0.10, "investigated auth-service — found JWT verification failures"
154
 
155
  if target == root_cause:
156
  if not self.grader_state["investigated_root_cause"]:
@@ -159,31 +148,28 @@ class IncidentResponseEnv:
159
  self.grader_state["identified_cascade_origin"] = True
160
  if self.task_id == "task_hard":
161
  self.grader_state["identified_root_cause"] = True
162
- return 0.3, f"investigated root cause: {target} — anomalies found"
163
  return 0.05, f"re-investigated {target} — no new findings"
164
 
165
  if target in scenario.get("available_services", []):
166
- return 0.05, f"investigated {target} — no critical issues found"
167
-
168
  return -0.05, f"unknown service: {target}"
169
 
170
  def _handle_postmortem(self, text: str) -> Tuple[float, str]:
171
  keywords = self.scenario.get("postmortem_keywords", [])
172
  if not keywords:
173
- # easy task doesn't require postmortem
174
- return 0.0, "postmortem not required for this task"
175
-
176
  text_lower = text.lower()
177
  matched = [kw for kw in keywords if kw in text_lower]
178
  quality = len(matched) / len(keywords)
179
- self.grader_state["postmortem_quality"] = max(self.grader_state.get("postmortem_quality", 0), quality)
180
-
 
181
  if quality >= 0.8:
182
- return 0.2, f"high quality postmortem ({len(matched)}/{len(keywords)} keywords)"
183
  elif quality >= 0.5:
184
- return 0.1, f"partial postmortem ({len(matched)}/{len(keywords)} keywords)"
185
- else:
186
- return 0.0, f"low quality postmortem ({len(matched)}/{len(keywords)} keywords)"
187
 
188
  def _is_task_complete(self) -> bool:
189
  gs = self.grader_state
 
19
  def _reset_state(self):
20
  self.step_count = 0
21
  self.done = False
22
+ self.cumulative_score = 0.01
23
  self.history = []
24
  self.grader_state: Dict[str, Any] = {
25
  "investigated_root_cause": False,
26
  "correct_escalation": False,
27
  "correct_fix_applied": False,
 
28
  "identified_cascade_origin": False,
29
  "identified_root_cause": False,
30
  "investigated_config_service": False,
 
40
  def state(self) -> StateModel:
41
  grader_fn = GRADERS[self.task_id]
42
  score, _ = grader_fn(self.grader_state)
 
43
  return StateModel(
44
  task_id=self.task_id,
45
  step=self.step_count,
 
53
  def step(self, action: Action) -> StepResult:
54
  if self.done:
55
  obs = self._build_observation("Episode already finished.")
56
+ return StepResult(
57
+ observation=obs,
58
+ reward=Reward(value=0.01, reason="already done"),
59
+ done=True,
60
+ info={},
61
+ )
62
 
63
  self.step_count += 1
64
  reward_value, reward_reason = self._process_action(action)
65
 
 
66
  if action.action_type == ActionType.NO_OP:
67
  self.grader_state["no_op_count"] += 1
68
+ reward_value = -0.05
69
  reward_reason = "no_op penalty"
70
 
 
71
  if self.grader_state["no_op_count"] >= 3:
72
+ reward_value = -0.10
73
  reward_reason = "repeated no_op — agent appears stuck"
74
 
75
  grader_fn = GRADERS[self.task_id]
76
  current_score, _ = grader_fn(self.grader_state)
 
77
 
 
78
  max_steps_reached = self.step_count >= self.meta["max_steps"]
79
  task_complete = self._is_task_complete()
80
 
81
  if task_complete:
82
  self.done = True
83
+ reward_reason += " | task complete"
 
84
 
85
  if max_steps_reached and not self.done:
86
  self.done = True
 
87
  reward_reason += " | max steps reached"
88
 
89
  self.cumulative_score = current_score
 
90
  self.history.append({
91
  "step": self.step_count,
92
  "action": action.model_dump(),
 
109
 
110
  if atype == ActionType.INVESTIGATE:
111
  return self._handle_investigate(target)
 
112
  elif atype == ActionType.ESCALATE:
113
  correct = scenario["correct_escalation"]
114
  if target == correct:
115
  if not self.grader_state["correct_escalation"]:
116
  self.grader_state["correct_escalation"] = True
117
+ return 0.30, f"correct escalation to {target}"
118
+ return 0.01, "already escalated correctly"
119
+ return -0.10, f"wrong escalation: {target}"
 
 
120
  elif atype == ActionType.APPLY_FIX:
121
  correct = scenario["correct_fix"]
122
  if target == correct:
123
  if not self.grader_state["correct_fix_applied"]:
124
  self.grader_state["correct_fix_applied"] = True
125
+ return 0.40, f"correct fix applied: {target}"
126
+ return 0.01, "fix already applied"
127
+ return -0.15, f"wrong fix: {target}"
 
 
128
  elif atype == ActionType.POSTMORTEM:
129
  return self._handle_postmortem(action.details or "")
130
+ return 0.01, "unknown action"
 
131
 
132
  def _handle_investigate(self, target: str) -> Tuple[float, str]:
133
  scenario = self.scenario
134
  root_cause = scenario["root_cause"]
135
 
 
136
  if self.task_id == "task_hard":
137
  if target == "config-service" and not self.grader_state["investigated_config_service"]:
138
  self.grader_state["investigated_config_service"] = True
139
+ return 0.15, "investigated config-service — partial secret rotation failure found"
140
  if target == "auth-service" and not self.grader_state["investigated_auth_service"]:
141
  self.grader_state["investigated_auth_service"] = True
142
+ return 0.10, "investigated auth-service — JWT verification failures found"
143
 
144
  if target == root_cause:
145
  if not self.grader_state["investigated_root_cause"]:
 
148
  self.grader_state["identified_cascade_origin"] = True
149
  if self.task_id == "task_hard":
150
  self.grader_state["identified_root_cause"] = True
151
+ return 0.30, f"investigated root cause: {target}"
152
  return 0.05, f"re-investigated {target} — no new findings"
153
 
154
  if target in scenario.get("available_services", []):
155
+ return 0.05, f"investigated {target} — no critical issues"
 
156
  return -0.05, f"unknown service: {target}"
157
 
158
  def _handle_postmortem(self, text: str) -> Tuple[float, str]:
159
  keywords = self.scenario.get("postmortem_keywords", [])
160
  if not keywords:
161
+ return 0.01, "postmortem not required for this task"
 
 
162
  text_lower = text.lower()
163
  matched = [kw for kw in keywords if kw in text_lower]
164
  quality = len(matched) / len(keywords)
165
+ self.grader_state["postmortem_quality"] = max(
166
+ self.grader_state.get("postmortem_quality", 0), quality
167
+ )
168
  if quality >= 0.8:
169
+ return 0.20, f"high quality postmortem ({len(matched)}/{len(keywords)} keywords)"
170
  elif quality >= 0.5:
171
+ return 0.10, f"partial postmortem ({len(matched)}/{len(keywords)} keywords)"
172
+ return 0.01, f"low quality postmortem ({len(matched)}/{len(keywords)} keywords)"
 
173
 
174
  def _is_task_complete(self) -> bool:
175
  gs = self.grader_state
app/models.py CHANGED
@@ -4,11 +4,11 @@ from enum import Enum
4
 
5
 
6
  class ActionType(str, Enum):
7
- INVESTIGATE = "investigate" # query a service/log
8
- ESCALATE = "escalate" # escalate to a team
9
- APPLY_FIX = "apply_fix" # apply a remediation
10
- POSTMORTEM = "postmortem" # submit root cause + summary
11
- NO_OP = "no_op" # do nothing (penalized)
12
 
13
 
14
  class Action(BaseModel):
@@ -29,7 +29,7 @@ class Observation(BaseModel):
29
 
30
 
31
  class Reward(BaseModel):
32
- value: float = Field(..., ge=-1.0, le=1.0)
33
  reason: str
34
 
35
 
@@ -44,7 +44,7 @@ class StateModel(BaseModel):
44
  task_id: str
45
  step: int
46
  max_steps: int
47
- score: float
48
  done: bool
49
  history: List[Dict[str, Any]] = Field(default_factory=list)
50
  grader_state: Dict[str, Any] = Field(default_factory=dict)
 
4
 
5
 
6
  class ActionType(str, Enum):
7
+ INVESTIGATE = "investigate"
8
+ ESCALATE = "escalate"
9
+ APPLY_FIX = "apply_fix"
10
+ POSTMORTEM = "postmortem"
11
+ NO_OP = "no_op"
12
 
13
 
14
  class Action(BaseModel):
 
29
 
30
 
31
  class Reward(BaseModel):
32
+ value: float = Field(..., description="Reward signal for this step")
33
  reason: str
34
 
35
 
 
44
  task_id: str
45
  step: int
46
  max_steps: int
47
+ score: float = Field(..., description="Task score strictly between 0 and 1 exclusive")
48
  done: bool
49
  history: List[Dict[str, Any]] = Field(default_factory=list)
50
  grader_state: Dict[str, Any] = Field(default_factory=dict)
app/tasks.py CHANGED
@@ -1,112 +1,74 @@
1
- """Task definitions and graders for each difficulty level."""
2
  from typing import Any, Dict, Tuple
3
 
4
 
 
 
 
 
 
5
  def grade_easy(grader_state: Dict[str, Any]) -> Tuple[float, str]:
6
- """
7
- Task 1 (Easy): Single service outage.
8
- Agent must:
9
- - Investigate postgres-db (0.3)
10
- - Escalate to database-team (0.3)
11
- - Apply fix: increase_db_connections (0.4)
12
- """
13
  score = 0.0
14
  reasons = []
15
-
16
  if grader_state.get("investigated_root_cause"):
17
- score += 0.3
18
- reasons.append("correctly investigated root cause service (+0.3)")
19
-
20
  if grader_state.get("correct_escalation"):
21
- score += 0.3
22
- reasons.append("escalated to correct team (+0.3)")
23
-
24
  if grader_state.get("correct_fix_applied"):
25
- score += 0.4
26
- reasons.append("applied correct fix (+0.4)")
27
-
28
- score = max(0.01, min(0.99, score))
29
- return round(score, 2), "; ".join(reasons) if reasons else "no progress"
30
 
31
 
32
  def grade_medium(grader_state: Dict[str, Any]) -> Tuple[float, str]:
33
- """
34
- Task 2 (Medium): Cascading failure.
35
- Agent must:
36
- - Investigate redis-cache (0.2)
37
- - Identify it as root cause (0.2)
38
- - Escalate to infra-team (0.2)
39
- - Apply fix: flush_redis_cache (0.2)
40
- - Submit postmortem mentioning redis/memory/oom (0.2)
41
- """
42
  score = 0.0
43
  reasons = []
44
-
45
  if grader_state.get("investigated_root_cause"):
46
- score += 0.2
47
- reasons.append("investigated redis-cache (+0.2)")
48
-
49
  if grader_state.get("identified_cascade_origin"):
50
- score += 0.2
51
- reasons.append("identified cascade origin (+0.2)")
52
-
53
  if grader_state.get("correct_escalation"):
54
- score += 0.2
55
- reasons.append("correct escalation (+0.2)")
56
-
57
  if grader_state.get("correct_fix_applied"):
58
- score += 0.2
59
- reasons.append("correct fix applied (+0.2)")
60
-
61
- if grader_state.get("postmortem_quality", 0) > 0:
62
- score += 0.2 * grader_state["postmortem_quality"]
63
- reasons.append(f"postmortem quality (+{0.2 * grader_state['postmortem_quality']:.2f})")
64
-
65
- score = max(0.01, min(0.99, score))
66
- return round(min(score, 1.0), 2), "; ".join(reasons) if reasons else "no progress"
67
 
68
 
69
  def grade_hard(grader_state: Dict[str, Any]) -> Tuple[float, str]:
70
- """
71
- Task 3 (Hard): Intermittent auth failure.
72
- Agent must:
73
- - Investigate config-service (0.15)
74
- - Investigate auth-service (0.1)
75
- - Identify root cause as config-service (0.2)
76
- - Escalate to security-team (0.15)
77
- - Apply correct fix: complete_secret_rotation (0.2)
78
- - Submit detailed postmortem with 4+ keywords (0.2)
79
- """
80
  score = 0.0
81
  reasons = []
82
-
83
  if grader_state.get("investigated_config_service"):
84
  score += 0.15
85
  reasons.append("investigated config-service (+0.15)")
86
-
87
  if grader_state.get("investigated_auth_service"):
88
  score += 0.10
89
  reasons.append("investigated auth-service (+0.10)")
90
-
91
  if grader_state.get("identified_root_cause"):
92
  score += 0.20
93
  reasons.append("identified root cause (+0.20)")
94
-
95
  if grader_state.get("correct_escalation"):
96
- score += 0.15
97
- reasons.append("correct escalation (+0.15)")
98
-
99
  if grader_state.get("correct_fix_applied"):
100
  score += 0.20
101
  reasons.append("correct fix applied (+0.20)")
102
-
103
- pm_quality = grader_state.get("postmortem_quality", 0)
104
- if pm_quality > 0:
105
- score += 0.20 * pm_quality
106
- reasons.append(f"postmortem quality (+{0.20 * pm_quality:.2f})")
107
-
108
- score = max(0.01, min(0.99, score))
109
- return round(min(score, 1.0), 2), "; ".join(reasons) if reasons else "no progress"
110
 
111
 
112
  GRADERS = {
 
1
+ """Task definitions and graders scores strictly between 0 and 1 exclusive."""
2
  from typing import Any, Dict, Tuple
3
 
4
 
5
+ def _strict(score: float) -> float:
6
+ """Ensure score is strictly between 0 and 1 (never 0.0 or 1.0)."""
7
+ return round(max(0.01, min(0.99, score)), 2)
8
+
9
+
10
  def grade_easy(grader_state: Dict[str, Any]) -> Tuple[float, str]:
 
 
 
 
 
 
 
11
  score = 0.0
12
  reasons = []
 
13
  if grader_state.get("investigated_root_cause"):
14
+ score += 0.32
15
+ reasons.append("investigated root cause (+0.32)")
 
16
  if grader_state.get("correct_escalation"):
17
+ score += 0.32
18
+ reasons.append("correct escalation (+0.32)")
 
19
  if grader_state.get("correct_fix_applied"):
20
+ score += 0.35
21
+ reasons.append("correct fix applied (+0.35)")
22
+ return _strict(score), "; ".join(reasons) if reasons else "no progress"
 
 
23
 
24
 
25
  def grade_medium(grader_state: Dict[str, Any]) -> Tuple[float, str]:
 
 
 
 
 
 
 
 
 
26
  score = 0.0
27
  reasons = []
 
28
  if grader_state.get("investigated_root_cause"):
29
+ score += 0.20
30
+ reasons.append("investigated root cause (+0.20)")
 
31
  if grader_state.get("identified_cascade_origin"):
32
+ score += 0.19
33
+ reasons.append("identified cascade origin (+0.19)")
 
34
  if grader_state.get("correct_escalation"):
35
+ score += 0.20
36
+ reasons.append("correct escalation (+0.20)")
 
37
  if grader_state.get("correct_fix_applied"):
38
+ score += 0.20
39
+ reasons.append("correct fix applied (+0.20)")
40
+ pm = grader_state.get("postmortem_quality", 0)
41
+ if pm > 0:
42
+ pts = round(0.20 * pm, 2)
43
+ score += pts
44
+ reasons.append(f"postmortem quality (+{pts})")
45
+ return _strict(score), "; ".join(reasons) if reasons else "no progress"
 
46
 
47
 
48
  def grade_hard(grader_state: Dict[str, Any]) -> Tuple[float, str]:
 
 
 
 
 
 
 
 
 
 
49
  score = 0.0
50
  reasons = []
 
51
  if grader_state.get("investigated_config_service"):
52
  score += 0.15
53
  reasons.append("investigated config-service (+0.15)")
 
54
  if grader_state.get("investigated_auth_service"):
55
  score += 0.10
56
  reasons.append("investigated auth-service (+0.10)")
 
57
  if grader_state.get("identified_root_cause"):
58
  score += 0.20
59
  reasons.append("identified root cause (+0.20)")
 
60
  if grader_state.get("correct_escalation"):
61
+ score += 0.14
62
+ reasons.append("correct escalation (+0.14)")
 
63
  if grader_state.get("correct_fix_applied"):
64
  score += 0.20
65
  reasons.append("correct fix applied (+0.20)")
66
+ pm = grader_state.get("postmortem_quality", 0)
67
+ if pm > 0:
68
+ pts = round(0.20 * pm, 2)
69
+ score += pts
70
+ reasons.append(f"postmortem quality (+{pts})")
71
+ return _strict(score), "; ".join(reasons) if reasons else "no progress"
 
 
72
 
73
 
74
  GRADERS = {