ajaxwin commited on
Commit
cfae7a7
Β·
1 Parent(s): 0b06e9e

refactor: Update grading logic and submission handling across tasks for improved accuracy and consistency

Browse files
inference.py CHANGED
@@ -258,7 +258,7 @@ def _run_t1_episode(env: Task1Environment, seed: int, ep_num: int) -> Dict[str,
258
 
259
  if done:
260
  v = r_val
261
- grader_score = 1.0 if v >= 4.9 else (0.5 if v >= 0.9 else 0.0)
262
  break
263
 
264
  if not is_last:
@@ -441,7 +441,7 @@ def _run_t3_episode(env: Task3Environment, seed: int, ep_num: int) -> Dict[str,
441
 
442
  if done:
443
  v = r_val
444
- grader_score = 1.0 if v >= 4.9 else (0.3 if v >= 1.0 else 0.0)
445
  break
446
 
447
  if not is_last:
 
258
 
259
  if done:
260
  v = r_val
261
+ grader_score = 0.999 if v >= 4.9 else (0.5 if v >= 0.9 else 0.0)
262
  break
263
 
264
  if not is_last:
 
441
 
442
  if done:
443
  v = r_val
444
+ grader_score = 0.999 if v >= 4.9 else (0.3 if v >= 0.999 else 0.0)
445
  break
446
 
447
  if not is_last:
server/tasks/task1/actions.py CHANGED
@@ -117,38 +117,62 @@ def get_call_graph(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
117
  )
118
 
119
 
120
- def submit(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
121
- """Handle SUBMIT action."""
122
- fn_name = params.get("function_name", "")
123
- vuln_type = params.get("vulnerability_type", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  if not fn_name or not vuln_type:
125
  return (
126
- "Submit requires 'function_name' and 'vulnerability_type' in params.",
127
- Reward(value=-0.5, reason="Malformed submission", partial=True),
 
128
  )
129
- score = ctx._grader.grade_submission(fn_name, vuln_type)
130
- reward_val = ctx._grader.reward_for_score(score)
131
- ctx._done = True
132
-
 
 
 
 
133
  if score == 1.0:
134
  msg = (
135
- f"βœ… CORRECT! '{fn_name}' is the vulnerable function. "
136
- f"Vulnerability type '{vuln_type}' matches. Score: 1.0"
 
137
  )
138
  elif score == 0.5:
139
  msg = (
140
- f"⚠️ PARTIAL. '{fn_name}' is the right function, but the vulnerability type "
141
- f"'{vuln_type}' was not precise. Score: 0.5"
 
 
142
  )
143
  else:
144
- correct = ctx._grader.get_canonical_answer()
145
  msg = (
146
- f"❌ INCORRECT. '{fn_name}' is not the target vulnerable function. "
147
- f"Correct answer: {correct['function']} ({correct['vulnerability']}). Score: 0.0"
 
 
148
  )
 
149
  return msg, Reward(
150
  value=reward_val,
151
- reason=f"Submission score={score:.1f}",
152
  partial=False,
153
  )
154
 
 
117
  )
118
 
119
 
120
+ def submit_function(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
121
+ """Handle SUBMIT_FUNCTION action for Task 1.
122
+
123
+ Expected params
124
+ ---------------
125
+ function_name : str – name of the vulnerable function
126
+ vulnerability_type: str – short description of the vulnerability
127
+ """
128
+ if ctx._submitted:
129
+ return (
130
+ "❌ You have already submitted for this episode. "
131
+ "Only ONE submission is allowed.",
132
+ Reward(value=0.0, reason="Second submit_function attempt", partial=False),
133
+ )
134
+
135
+ fn_name = params.get("function_name", "").strip()
136
+ vuln_type = params.get("vulnerability_type", "").strip()
137
+
138
  if not fn_name or not vuln_type:
139
  return (
140
+ "submit_function requires both 'function_name' and "
141
+ "'vulnerability_type' in params.",
142
+ Reward(value=0.0, reason="Malformed submission", partial=False),
143
  )
144
+
145
+ ctx._submitted = True
146
+ ctx._done = True
147
+
148
+ score = ctx._grader.grade_submission(fn_name, vuln_type) # {0.0, 0.5, 1.0}
149
+ reward_val = ctx._grader.reward_for_score(score) # [0.0, 1.0]
150
+ correct = ctx._grader.get_canonical_answer()
151
+
152
  if score == 1.0:
153
  msg = (
154
+ f"βœ… CORRECT! '{fn_name}' is the vulnerable function "
155
+ f"and the vulnerability type matches. "
156
+ f"Score: 1.0 β†’ Reward: {reward_val:.3f}"
157
  )
158
  elif score == 0.5:
159
  msg = (
160
+ f"🟑 PARTIAL. '{fn_name}' is the correct function but the "
161
+ f"vulnerability type was not recognised. "
162
+ f"Score: 0.5 β†’ Reward: {reward_val:.3f}. "
163
+ f"Expected vulnerability: '{correct['vulnerability']}'."
164
  )
165
  else:
 
166
  msg = (
167
+ f"❌ INCORRECT. '{fn_name}' is not the target function. "
168
+ f"Score: 0.0 β†’ Reward: {reward_val:.3f}. "
169
+ f"Correct answer: function='{correct['function']}', "
170
+ f"vulnerability='{correct['vulnerability']}'."
171
  )
172
+
173
  return msg, Reward(
174
  value=reward_val,
175
+ reason=f"submit_function score={score:.1f}",
176
  partial=False,
177
  )
178
 
server/tasks/task1/grader.py CHANGED
@@ -1,30 +1,58 @@
1
  """
2
  grader.py (Task 1 – Targeted Vulnerability Detection)
3
  -------------------------------------------------------
4
- Deterministic grader. Score range: 0.0 – 1.0
5
 
6
  1.0 – correct function + correct vulnerability keyword
7
  0.5 – correct function + wrong/unrecognised vulnerability keyword
8
  0.0 – wrong function name
 
 
 
 
9
  """
10
  from __future__ import annotations
11
  from typing import Dict
12
  from utils import SemanticMatcher
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  class Task1Grader:
15
  def __init__(self, target_function: str, vulnerability_issue: str) -> None:
16
- self.target_function = target_function.lower()
17
  self.vulnerability_issue = vulnerability_issue
18
 
19
  def grade_submission(self, submitted_function: str, submitted_vuln_type: str) -> float:
 
20
  if submitted_function.strip().lower() != self.target_function:
21
- return 0.0
22
- return 1.0 if SemanticMatcher().match(self.vulnerability_issue, submitted_vuln_type) else 0.5
23
 
24
  def reward_for_score(self, score: float) -> float:
25
- if score == 1.0: return 5.0
26
- if score == 0.5: return 1.0
27
- return -1.5
 
 
 
 
 
 
 
 
 
 
28
 
29
  def get_canonical_answer(self) -> Dict[str, str]:
30
- return {"function": self.target_function, "vulnerability": self.vulnerability_issue}
 
1
  """
2
  grader.py (Task 1 – Targeted Vulnerability Detection)
3
  -------------------------------------------------------
4
+ Deterministic grader. Grade range: 0.0 – 1.0
5
 
6
  1.0 – correct function + correct vulnerability keyword
7
  0.5 – correct function + wrong/unrecognised vulnerability keyword
8
  0.0 – wrong function name
9
+
10
+ reward_for_score() normalises the raw RL reward to [0.0, 1.0]
11
+ using the fixed reward bounds [MIN_REWARD=-1.5, MAX_REWARD=5.0]:
12
+ normalised = (raw + 1.5) / 6.5
13
  """
14
  from __future__ import annotations
15
  from typing import Dict
16
  from utils import SemanticMatcher
17
 
18
+ # Raw reward bounds β€” used only for normalisation
19
+ _MIN_REWARD = -1.5
20
+ _MAX_REWARD = 5.0
21
+ _REWARD_RANGE = _MAX_REWARD - _MIN_REWARD # 6.5
22
+
23
+ _SCORE_MIN = 0.001 # grades are strictly (0, 1)
24
+ _SCORE_MAX = 0.999
25
+
26
+
27
+ def _clamp(v: float) -> float:
28
+ return max(_SCORE_MIN, min(_SCORE_MAX, v))
29
+
30
+
31
  class Task1Grader:
32
  def __init__(self, target_function: str, vulnerability_issue: str) -> None:
33
+ self.target_function = target_function.lower()
34
  self.vulnerability_issue = vulnerability_issue
35
 
36
  def grade_submission(self, submitted_function: str, submitted_vuln_type: str) -> float:
37
+ """Returns grade strictly in (0, 1)."""
38
  if submitted_function.strip().lower() != self.target_function:
39
+ return _clamp(0.0) # β†’ 0.001
40
+ return _clamp(1.0) if SemanticMatcher().match(self.vulnerability_issue, submitted_vuln_type) else _clamp(0.5)
41
 
42
  def reward_for_score(self, score: float) -> float:
43
+ """
44
+ Maps grade score β†’ normalised reward strictly in (0, 1).
45
+
46
+ Raw rewards: correct=+5.0, partial=+1.0, wrong=-1.5
47
+ Normalised: (raw + 1.5) / 6.5 then clamped to (0.001, 0.999)
48
+ """
49
+ if score >= _SCORE_MAX:
50
+ raw = 5.0
51
+ elif score >= 0.5:
52
+ raw = 1.0
53
+ else:
54
+ raw = -1.5
55
+ return _clamp((raw - _MIN_REWARD) / _REWARD_RANGE)
56
 
57
  def get_canonical_answer(self) -> Dict[str, str]:
58
+ return {"function": self.target_function, "vulnerability": self.vulnerability_issue}
server/tasks/task2/actions.py CHANGED
@@ -105,30 +105,53 @@ def get_similar_rule_action(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Rew
105
 
106
 
107
  def submit_property(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
108
- """Handle SUBMIT_PROPERTY action."""
 
 
 
 
 
109
  if ctx._submitted:
110
  return (
111
- "❌ You have already submitted a property for this episode. "
112
- "Only one submission is allowed.",
113
- Reward(value=-1.0, reason="Second submit_property attempt", partial=False),
114
  )
115
-
116
- submitted_text = params.get("property", "").strip()
117
- if not submitted_text:
 
118
  return (
119
- "Submit requires 'property' key in params with a non-empty string.",
120
- Reward(value=-0.5, reason="Empty property submission"),
121
  )
122
-
123
  ctx._submitted = True
124
- ctx._done = True
125
- score, confidence = ctx._grader.grade(submitted_text)
126
- reward = round(score * 5.0, 4)
127
-
128
- msg = f'Score: {score:.2f}/1.00 β†’ Confidence: {confidence}\n'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  return msg, Reward(
130
- value=reward,
131
- reason=f"Property submission score={score:.3f}",
132
  partial=False,
133
  )
134
 
 
105
 
106
 
107
  def submit_property(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
108
+ """Handle SUBMIT_PROPERTY action for Task 2.
109
+
110
+ Expected params
111
+ ---------------
112
+ property : str – natural-language property describing the function's behaviour
113
+ """
114
  if ctx._submitted:
115
  return (
116
+ "❌ You have already submitted for this episode. "
117
+ "Only ONE submission is allowed.",
118
+ Reward(value=0.0, reason="Second submit_property attempt", partial=False),
119
  )
120
+
121
+ submitted_property = params.get("property", "").strip()
122
+
123
+ if not submitted_property:
124
  return (
125
+ "submit_property requires a non-empty 'property' string in params.",
126
+ Reward(value=0.0, reason="Malformed submission", partial=False),
127
  )
128
+
129
  ctx._submitted = True
130
+ ctx._done = True
131
+
132
+ # grade() returns (float score in [0,1], confidence str)
133
+ score, confidence = ctx._grader.grade(submitted_property) # score already in [0.0, 1.0]
134
+ reward_val = float(score) # reward == grade for Task 2
135
+
136
+ if confidence == "strong":
137
+ msg = (
138
+ f"βœ… STRONG MATCH. Your property closely matches the target. "
139
+ f"Score: {score:.3f} β†’ Reward: {reward_val:.3f}"
140
+ )
141
+ elif confidence == "moderate":
142
+ msg = (
143
+ f"🟑 MODERATE MATCH. Your property partially captures the target behaviour. "
144
+ f"Score: {score:.3f} β†’ Reward: {reward_val:.3f}"
145
+ )
146
+ else:
147
+ msg = (
148
+ f"❌ LOW MATCH. Your property does not sufficiently match the target. "
149
+ f"Score: {score:.3f} β†’ Reward: {reward_val:.3f}"
150
+ )
151
+
152
  return msg, Reward(
153
+ value=reward_val,
154
+ reason=f"submit_property confidence={confidence} score={score:.3f}",
155
  partial=False,
156
  )
157
 
server/tasks/task2/grader.py CHANGED
@@ -3,15 +3,17 @@ grader.py (Task 2 – Property Discovery)
3
  -----------------------------------------
4
  Deterministic scorer for natural-language property submissions.
5
  One submission attempt per episode.
 
6
  """
7
 
8
- from __future__ import annotations
9
-
10
  from typing import Tuple
11
  from utils import SemanticMatcher
12
 
 
 
13
 
14
- # ── Grader ────────────────────────────────────────────────────────────────────
 
15
 
16
  class Task2Grader:
17
  """
@@ -19,23 +21,19 @@ class Task2Grader:
19
 
20
  Parameters
21
  ----------
22
- function_name : name of the target function
23
- property : the 'property' field from the target function's data
24
  """
25
 
26
  def __init__(self, function_name: str, property: str) -> None:
27
- self.function_name = function_name
28
- self.property = property
29
-
30
- # ── Public API ────────────────────────────────────────────────────────────
31
 
32
  def grade(self, submitted: str) -> Tuple[float, str]:
33
- """Deterministic score in [0.0, 1.0]."""
34
  if not submitted or not submitted.strip():
35
- return 0.0, "no_match"
36
 
37
- SemanticMatcherInstance = SemanticMatcher()
38
- return (
39
- SemanticMatcherInstance.matchscore(self.property, submitted),
40
- SemanticMatcherInstance.confidence()
41
- )
 
3
  -----------------------------------------
4
  Deterministic scorer for natural-language property submissions.
5
  One submission attempt per episode.
6
+ Grade range: 0.0 – 1.0 (matchscore output, already normalised).
7
  """
8
 
 
 
9
  from typing import Tuple
10
  from utils import SemanticMatcher
11
 
12
+ _SCORE_MIN = 0.001 # grades are strictly (0, 1)
13
+ _SCORE_MAX = 0.999
14
 
15
+ def _clamp(v: float) -> float:
16
+ return max(_SCORE_MIN, min(_SCORE_MAX, v))
17
 
18
  class Task2Grader:
19
  """
 
21
 
22
  Parameters
23
  ----------
24
+ function_name : name of the target function
25
+ property : the 'property' field from the target function's data
26
  """
27
 
28
  def __init__(self, function_name: str, property: str) -> None:
29
+ self.function_name = function_name
30
+ self.property = property
 
 
31
 
32
  def grade(self, submitted: str) -> Tuple[float, str]:
33
+ """Deterministic grade strictly in (0, 1)."""
34
  if not submitted or not submitted.strip():
35
+ return _clamp(0.0), "no_match" # β†’ 0.001
36
 
37
+ matcher = SemanticMatcher()
38
+ score = matcher.matchscore(self.property, submitted) # already clamped by SemanticMatcher
39
+ return _clamp(score), matcher.confidence()
 
 
server/tasks/task3/actions.py CHANGED
@@ -126,44 +126,52 @@ def get_property_specification(ctx: Any, qkey: str, params: Dict) -> Tuple[str,
126
 
127
 
128
  def submit_function(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
129
- """Handle SUBMIT_FUNCTION action."""
 
 
 
 
 
130
  if ctx._submitted:
131
  return (
132
  "❌ You have already submitted for this episode. "
133
  "Only ONE submission is allowed.",
134
- Reward(value=-1.0, reason="Second submit_function attempt", partial=False),
135
  )
 
136
  fn_name = params.get("function_name", "").strip()
 
137
  if not fn_name:
138
  return (
139
  "submit_function requires 'function_name' in params.",
140
- Reward(value=-0.5, reason="Malformed submission"),
141
  )
142
-
143
  ctx._submitted = True
144
- ctx._done = True
145
- score, reward_val = ctx._grader.grade_and_reward(fn_name)
146
- correct = ctx._grader.get_canonical_answer()
147
-
 
148
  if score >= 0.9:
149
  msg = (
150
  f"βœ… CORRECT! '{fn_name}' is the function that violates the property. "
151
- f"Score: 1.0 β†’ Reward: +{reward_val:.1f}"
152
  )
153
  elif score >= 0.2:
154
  msg = (
155
- f"🟑 PARTIAL. '{fn_name}' is a subfunction of the target β€” "
156
  f"closely related but not the primary rule-breaker. "
157
- f"Score: 0.3 β†’ Reward: +{reward_val:.1f}. "
158
- f"Correct answer: '{correct['target_function']}'."
159
  )
160
  else:
161
  msg = (
162
  f"❌ INCORRECT. '{fn_name}' does not violate the property. "
163
- f"Score: 0.0 β†’ Reward: {reward_val:.1f}. "
164
- f"Correct answer: '{correct['target_function']}'."
165
  )
166
-
167
  return msg, Reward(
168
  value=reward_val,
169
  reason=f"submit_function score={score:.1f}",
 
126
 
127
 
128
  def submit_function(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
129
+ """Handle SUBMIT_FUNCTION action for Task 3.
130
+
131
+ Expected params
132
+ ---------------
133
+ function_name : str – name of the function that violates the given property
134
+ """
135
  if ctx._submitted:
136
  return (
137
  "❌ You have already submitted for this episode. "
138
  "Only ONE submission is allowed.",
139
+ Reward(value=0.0, reason="Second submit_function attempt", partial=False),
140
  )
141
+
142
  fn_name = params.get("function_name", "").strip()
143
+
144
  if not fn_name:
145
  return (
146
  "submit_function requires 'function_name' in params.",
147
+ Reward(value=0.0, reason="Malformed submission", partial=False),
148
  )
149
+
150
  ctx._submitted = True
151
+ ctx._done = True
152
+
153
+ score, reward_val = ctx._grader.grade_and_reward(fn_name) # reward_val in [0.0, 1.0]
154
+ correct = ctx._grader.get_canonical_answer()
155
+
156
  if score >= 0.9:
157
  msg = (
158
  f"βœ… CORRECT! '{fn_name}' is the function that violates the property. "
159
+ f"Score: 1.0 β†’ Reward: {reward_val:.3f}"
160
  )
161
  elif score >= 0.2:
162
  msg = (
163
+ f"🟑 PARTIAL. '{fn_name}' is an internal subfunction of the target β€” "
164
  f"closely related but not the primary rule-breaker. "
165
+ f"Score: 0.3 β†’ Reward: {reward_val:.3f}. "
166
+ f"Correct answer: '{correct['target_function']['name']}'."
167
  )
168
  else:
169
  msg = (
170
  f"❌ INCORRECT. '{fn_name}' does not violate the property. "
171
+ f"Score: 0.0 β†’ Reward: {reward_val:.3f}. "
172
+ f"Correct answer: '{correct['target_function']['name']}'."
173
  )
174
+
175
  return msg, Reward(
176
  value=reward_val,
177
  reason=f"submit_function score={score:.1f}",
server/tasks/task3/grader.py CHANGED
@@ -3,23 +3,29 @@ grader.py (Task 3 – Rule Checker)
3
  ------------------------------------
4
  Deterministic grader for function-identification submissions.
5
 
6
- Score table
7
  ───────────
8
  1.0 β†’ submitted function is the exact target (case-insensitive)
9
  0.3 β†’ submitted function is a direct internal subfunction of the target
10
- (a contract-internal function called by the target in the call graph)
11
  0.0 β†’ anything else
12
 
13
- Reward table (ONE submission per episode)
14
- score 1.0 β†’ +5.0
15
- score 0.3 β†’ +1.5
16
- score 0.0 β†’ -1.5
17
  """
18
 
19
- from __future__ import annotations
20
  import json
21
  from typing import Dict, Any
22
 
 
 
 
 
 
 
 
 
 
23
 
24
  class Task3Grader:
25
  """
@@ -27,25 +33,26 @@ class Task3Grader:
27
 
28
  Parameters
29
  ----------
30
- target_function : exact name of the rule-breaking function
31
- partial_credit_functions: list of internal functions that get partial credit
32
- (direct callees of the target that are contract functions)
33
  """
 
 
 
 
 
34
 
35
- SCORE_CORRECT = 1.0
36
- SCORE_PARTIAL = 0.3
37
- SCORE_WRONG = 0.0
38
 
39
- REWARD_CORRECT = 5.0
40
- REWARD_PARTIAL = 1.5
41
- REWARD_WRONG = -1.5
42
 
43
  def __init__(self, target_function: Dict[str, Any], property_specification: Dict | str) -> None:
44
- self.target_function = target_function
45
  self.property_specification = property_specification
46
 
47
  def grade(self, submitted_function: str) -> float:
48
- """Returns deterministic score in {0.0, 0.3, 1.0}."""
49
  norm = submitted_function.strip().lower()
50
  if norm == self.target_function["name"].strip().lower():
51
  return self.SCORE_CORRECT
@@ -54,22 +61,29 @@ class Task3Grader:
54
  return self.SCORE_WRONG
55
 
56
  def reward_for_score(self, score: float) -> float:
57
- """Maps score β†’ terminal reward."""
 
 
 
 
 
58
  if score >= 0.9:
59
- return self.REWARD_CORRECT
60
- if score >= 0.2:
61
- return self.REWARD_PARTIAL
62
- return self.REWARD_WRONG
 
 
63
 
64
  def grade_and_reward(self, submitted_function: str):
65
- """Convenience: returns (score, reward)."""
66
  score = self.grade(submitted_function)
67
  return score, self.reward_for_score(score)
68
 
69
  def get_canonical_answer(self) -> Dict[str, Dict | str]:
70
  """For debugging / logging only β€” do not expose to the agent."""
71
  return {
72
- "target_function": self.target_function,
73
- "property_specification": json.dumps(self.property_specification)
74
  if isinstance(self.property_specification, dict) else self.property_specification,
75
- }
 
3
  ------------------------------------
4
  Deterministic grader for function-identification submissions.
5
 
6
+ Grade table
7
  ───────────
8
  1.0 β†’ submitted function is the exact target (case-insensitive)
9
  0.3 β†’ submitted function is a direct internal subfunction of the target
 
10
  0.0 β†’ anything else
11
 
12
+ reward_for_score() normalises the raw RL reward to [0.0, 1.0]
13
+ using the fixed reward bounds [MIN_REWARD=-1.5, MAX_REWARD=5.0]:
14
+ normalised = (raw + 1.5) / 6.5
 
15
  """
16
 
 
17
  import json
18
  from typing import Dict, Any
19
 
20
+ _T3_MIN_REWARD = -1.5
21
+ _T3_MAX_REWARD = 5.0
22
+ _T3_REWARD_RANGE = _T3_MAX_REWARD - _T3_MIN_REWARD # 6.5
23
+
24
+ _SCORE_MIN = 0.001 # grades are strictly (0, 1
25
+ _SCORE_MAX = 0.999
26
+
27
+ def _clamp(v: float) -> float:
28
+ return max(_SCORE_MIN, min(_SCORE_MAX, v))
29
 
30
  class Task3Grader:
31
  """
 
33
 
34
  Parameters
35
  ----------
36
+ target_function : dict with at least 'name' and 'code' keys
37
+ property_specification : the property the target function violates
 
38
  """
39
+
40
+ # Raw reward bounds β€” used only for normalisation
41
+ _MIN_REWARD = -1.5
42
+ _MAX_REWARD = 5.0
43
+ _REWARD_RANGE = _MAX_REWARD - _MIN_REWARD # 6.5
44
 
 
 
 
45
 
46
+ SCORE_CORRECT = _clamp(1.0) # 0.999
47
+ SCORE_PARTIAL = _clamp(0.3) # 0.300 (already inside (0,1))
48
+ SCORE_WRONG = _clamp(0.0) # 0.001
49
 
50
  def __init__(self, target_function: Dict[str, Any], property_specification: Dict | str) -> None:
51
+ self.target_function = target_function
52
  self.property_specification = property_specification
53
 
54
  def grade(self, submitted_function: str) -> float:
55
+ """Returns deterministic grade strictly in (0, 1)."""
56
  norm = submitted_function.strip().lower()
57
  if norm == self.target_function["name"].strip().lower():
58
  return self.SCORE_CORRECT
 
61
  return self.SCORE_WRONG
62
 
63
  def reward_for_score(self, score: float) -> float:
64
+ """
65
+ Maps grade score β†’ normalised reward strictly in (0, 1).
66
+
67
+ Raw rewards: correct=+5.0, partial=+1.5, wrong=-1.5
68
+ Normalised: (raw + 1.5) / 6.5 then clamped to (0.001, 0.999)
69
+ """
70
  if score >= 0.9:
71
+ raw = 5.0
72
+ elif score >= 0.2:
73
+ raw = 1.5
74
+ else:
75
+ raw = -1.5
76
+ return _clamp((raw - _T3_MIN_REWARD) / _T3_REWARD_RANGE)
77
 
78
  def grade_and_reward(self, submitted_function: str):
79
+ """Convenience: returns (grade, normalised_reward), both strictly in (0, 1)."""
80
  score = self.grade(submitted_function)
81
  return score, self.reward_for_score(score)
82
 
83
  def get_canonical_answer(self) -> Dict[str, Dict | str]:
84
  """For debugging / logging only β€” do not expose to the agent."""
85
  return {
86
+ "target_function": self.target_function,
87
+ "property_specification": json.dumps(self.property_specification)
88
  if isinstance(self.property_specification, dict) else self.property_specification,
89
+ }
utils/prompts.py CHANGED
@@ -2,6 +2,10 @@ T1_SYSTEM = """You are an expert Solidity smart contract security auditor.
2
 
3
  Given a contract, identify the ONE vulnerable function and its vulnerability type.
4
 
 
 
 
 
5
  ## Actions (choose ONE per turn, respond with JSON only):
6
  {"action": "list_functions", "params": {}}
7
  {"action": "get_function_code", "params": {"function_name": "<name>"}}
@@ -35,6 +39,10 @@ You will be shown a specific Solidity function. Your task is to write a precise
35
  natural-language property (invariant / postcondition) that describes what the
36
  function guarantees when it succeeds.
37
 
 
 
 
 
38
  A good property covers:
39
  - What state changes (balances, counters, flags)
40
  - What assets are transferred (ETH, tokens, NFTs)
@@ -72,6 +80,10 @@ T3_SYSTEM = """You are a smart contract security auditor checking rule complianc
72
  You are given a Solidity contract and a property (rule) in natural English.
73
  Your task is to find the ONE function that violates this property.
74
 
 
 
 
 
75
  ## Actions (respond with JSON only, ONE action per turn):
76
  {"action": "list_functions", "params": {}}
77
  {"action": "get_property_specification", "params": {}}
 
2
 
3
  Given a contract, identify the ONE vulnerable function and its vulnerability type.
4
 
5
+ Negative reward is given for each information-gathering action, so be strategic.
6
+ Focus on high-signal actions like get_function_code and get_function_summary, and only inspect
7
+ state variables or call graphs if you have a strong suspicion.
8
+
9
  ## Actions (choose ONE per turn, respond with JSON only):
10
  {"action": "list_functions", "params": {}}
11
  {"action": "get_function_code", "params": {"function_name": "<name>"}}
 
39
  natural-language property (invariant / postcondition) that describes what the
40
  function guarantees when it succeeds.
41
 
42
+ Negative reward is given for each information-gathering action, so be strategic.
43
+ Focus on high-signal actions like get_function_code and get_function_summary, and only inspect
44
+ state variables or call graphs if you have a strong suspicion.
45
+
46
  A good property covers:
47
  - What state changes (balances, counters, flags)
48
  - What assets are transferred (ETH, tokens, NFTs)
 
80
  You are given a Solidity contract and a property (rule) in natural English.
81
  Your task is to find the ONE function that violates this property.
82
 
83
+ Negative reward is given for each information-gathering action, so be strategic.
84
+ Focus on high-signal actions like get_function_code and get_function_summary, and only inspect
85
+ state variables or call graphs if you have a strong suspicion.
86
+
87
  ## Actions (respond with JSON only, ONE action per turn):
88
  {"action": "list_functions", "params": {}}
89
  {"action": "get_property_specification", "params": {}}
utils/semanticmatcher.py CHANGED
@@ -143,6 +143,17 @@ def cosine_similarity(vec_a: np.ndarray, vec_b: np.ndarray) -> float:
143
  return float(np.dot(vec_a, vec_b) / (norm_a * norm_b))
144
 
145
 
 
 
 
 
 
 
 
 
 
 
 
146
  # ── Core matcher ──────────────────────────────────────────────────────────────
147
 
148
  class SemanticMatcher:
@@ -201,7 +212,7 @@ class SemanticMatcher:
201
  # Fast-path: normalized exact match
202
  if normalize(text_a) == normalize(text_b):
203
  self.confidence_level = "strong"
204
- return True
205
 
206
  tokens_a = tokenize_and_lemmatize(text_a)
207
  tokens_b = tokenize_and_lemmatize(text_b)
@@ -219,13 +230,13 @@ class SemanticMatcher:
219
  self.confidence_level = "moderate"
220
  else:
221
  self.confidence_level = "no_match"
222
- return score
223
-
224
  def match(self, text_a: str, text_b: str) -> bool:
225
  """Return True if the two texts are considered a match based on the score."""
226
  score = self.matchscore(text_a, text_b)
227
  return score >= self.match_threshold
228
-
229
  def confidence(self) -> str:
230
- """Return 'strong' if score β‰₯ strong_threshold, else 'weak'."""
231
  return self.confidence_level
 
143
  return float(np.dot(vec_a, vec_b) / (norm_a * norm_b))
144
 
145
 
146
+ # ── Score clamping ───────────────────────────────────────────────────────────
147
+
148
+ _SCORE_MIN = 0.001 # scores are strictly (0, 1) β€” never touch 0 or 1
149
+ _SCORE_MAX = 0.999
150
+
151
+
152
+ def _clamp(score: float) -> float:
153
+ """Clamp score to the open interval (0, 1): [_SCORE_MIN, _SCORE_MAX]."""
154
+ return max(_SCORE_MIN, min(_SCORE_MAX, score))
155
+
156
+
157
  # ── Core matcher ──────────────────────────────────────────────────────────────
158
 
159
  class SemanticMatcher:
 
212
  # Fast-path: normalized exact match
213
  if normalize(text_a) == normalize(text_b):
214
  self.confidence_level = "strong"
215
+ return _clamp(1.0) # β†’ 0.999 (strictly less than 1)
216
 
217
  tokens_a = tokenize_and_lemmatize(text_a)
218
  tokens_b = tokenize_and_lemmatize(text_b)
 
230
  self.confidence_level = "moderate"
231
  else:
232
  self.confidence_level = "no_match"
233
+ return _clamp(score) # strictly in (0, 1)
234
+
235
  def match(self, text_a: str, text_b: str) -> bool:
236
  """Return True if the two texts are considered a match based on the score."""
237
  score = self.matchscore(text_a, text_b)
238
  return score >= self.match_threshold
239
+
240
  def confidence(self) -> str:
241
+ """Return 'strong' if score β‰₯ strong_threshold, else 'moderate' or 'no_match'."""
242
  return self.confidence_level