Update server/code_assessment_environment.py

#15
Files changed (1) hide show
  1. server/code_assessment_environment.py +57 -34
server/code_assessment_environment.py CHANGED
@@ -597,11 +597,11 @@ class CodeAssessmentEnvironment(Environment):
597
  expected_output=None,
598
  feedback="Welcome! Evaluate the AI response and submit your judgment.",
599
  is_correct=False,
600
- partial_credit=0.0,
601
  problems_solved=0,
602
  current_streak=0,
603
  done=False,
604
- reward=0.0,
605
  )
606
 
607
  def step(self, action: CodeAssessmentAction) -> CodeAssessmentObservation: # type: ignore[override]
@@ -611,8 +611,8 @@ class CodeAssessmentEnvironment(Environment):
611
 
612
  is_correct, partial_credit, feedback = self._grade(task_type, action.answer, problem)
613
 
614
- reward = self._calculate_reward(is_correct, partial_credit)
615
- self._total_reward += reward
616
 
617
  if is_correct:
618
  self._problems_solved += 1
@@ -623,8 +623,11 @@ class CodeAssessmentEnvironment(Environment):
623
  done = self._state.step_count >= self.MAX_STEPS
624
  expected_str = self._format_expected(task_type, problem)
625
 
 
 
 
626
  if is_correct:
627
- self._advance()
628
 
629
  next_task = TASK_TYPES[self._difficulty]
630
  p = self._current_problem
@@ -644,8 +647,9 @@ class CodeAssessmentEnvironment(Environment):
644
  problems_solved=self._problems_solved,
645
  current_streak=self._current_streak,
646
  done=done,
647
- reward=reward,
648
  metadata={
 
649
  "total_reward": self._total_reward,
650
  "step": self._state.step_count,
651
  "task_type": next_task,
@@ -670,16 +674,24 @@ class CodeAssessmentEnvironment(Environment):
670
  scores = problem["expected_scores"]
671
  return ", ".join(f"{k}={v}" for k, v in scores.items())
672
 
 
 
 
 
 
 
 
673
  # ------------------------------------------------------------------
674
  # Grading dispatch
675
  # ------------------------------------------------------------------
676
  def _grade(self, task_type: str, answer: str, problem: Dict) -> Tuple[bool, float, str]:
677
  if task_type == "correctness_check":
678
- return self._grade_correctness(answer, problem)
679
  elif task_type == "tone_appropriateness":
680
- return self._grade_tone(answer, problem)
681
  else:
682
- return self._grade_multi_dimensional(answer, problem)
 
683
 
684
  # ── Task 1: Correctness Check ─────────────────────────────────────
685
  def _grade_correctness(self, answer: str, problem: Dict) -> Tuple[bool, float, str]:
@@ -695,7 +707,7 @@ class CodeAssessmentEnvironment(Environment):
695
  r_match = expected_r in given_r or given_r in expected_r
696
 
697
  if j_match and r_match:
698
- return True, 1.0, f"Correct! {problem['explanation']}"
699
  if j_match:
700
  return False, 0.6, f"Judgment correct, wrong reason. Expected reason: '{expected_r}'. {problem['explanation']}"
701
  if r_match:
@@ -704,7 +716,7 @@ class CodeAssessmentEnvironment(Environment):
704
  VALID = {"correct", "incorrect", "partially-correct"}
705
  if given_j in VALID:
706
  return False, 0.2, f"Wrong. Expected: '{expected_j}, {expected_r}'. {problem['explanation']}"
707
- return False, 0.0, f"Invalid format. Expected: '{expected_j}, {expected_r}'. {problem['explanation']}"
708
 
709
  # ── Task 2: Tone & Audience Appropriateness ───────────────────────
710
  def _grade_tone(self, answer: str, problem: Dict) -> Tuple[bool, float, str]:
@@ -732,7 +744,7 @@ class CodeAssessmentEnvironment(Environment):
732
  # Score issues via F1
733
  if "none" in expected_issues:
734
  if found_issues <= {"none"} or not found_issues:
735
- issues_score = 1.0
736
  else:
737
  found_issues.discard("none")
738
  issues_score = 0.2 # false positives
@@ -741,15 +753,15 @@ class CodeAssessmentEnvironment(Environment):
741
  tp = len(found_issues & expected_issues)
742
  fp = len(found_issues - expected_issues)
743
  fn = len(expected_issues - found_issues)
744
- prec = tp / (tp + fp) if (tp + fp) else 0.0
745
- rec = tp / (tp + fn) if (tp + fn) else 0.0
746
- issues_score = (2 * prec * rec / (prec + rec)) if (prec + rec) else 0.0
747
 
748
  # Combined score: 50% rating + 50% issues
749
- score = (0.5 if rating_match else 0.0) + 0.5 * issues_score
750
 
751
- if rating_match and issues_score >= 0.99:
752
- return True, 1.0, f"Correct! {problem['explanation']}"
753
 
754
  parts_fb = []
755
  if not rating_match:
@@ -777,7 +789,7 @@ class CodeAssessmentEnvironment(Environment):
777
 
778
  parsed_count = sum(1 for v in given.values() if v is not None)
779
  if parsed_count == 0:
780
- return False, 0.0, (
781
  f"Could not parse scores. Expected format: correctness=N, tone=N, empathy=N, safety=N. "
782
  f"Expected: {self._format_expected('multi_dimensional', problem)}. "
783
  f"{problem['explanation']}"
@@ -790,31 +802,31 @@ class CodeAssessmentEnvironment(Environment):
790
  exp = expected[dim]
791
  got = given[dim]
792
  if got is None:
793
- dim_scores[dim] = 0.0
794
  dim_feedback.append(f"{dim}: missing (expected {exp})")
795
  continue
796
 
797
  diff = abs(exp - got)
798
  if diff <= 1:
799
- dim_scores[dim] = 1.0
800
  elif diff <= 2:
801
  dim_scores[dim] = 0.7
802
  elif diff <= 3:
803
  dim_scores[dim] = 0.4
804
  else:
805
- dim_scores[dim] = max(0.0, 1.0 - diff / 10.0)
806
 
807
  if diff > 1:
808
  dim_feedback.append(f"{dim}: gave {got}, expected {exp} (off by {diff})")
809
 
810
  overall = sum(dim_scores.values()) / 4.0
811
- all_close = all(s >= 1.0 for s in dim_scores.values())
812
 
813
  if all_close:
814
- return True, 1.0, f"Excellent! All dimensions within Β±1. {problem['explanation']}"
815
 
816
  detail = ". ".join(dim_feedback) if dim_feedback else "Close on all dimensions"
817
- return False, round(overall, 2), (
818
  f"Score: {overall:.0%}. {detail}. {problem['explanation']}"
819
  )
820
 
@@ -822,6 +834,7 @@ class CodeAssessmentEnvironment(Environment):
822
  # Reward
823
  # ------------------------------------------------------------------
824
  def _calculate_reward(self, is_correct: bool, score: float) -> float:
 
825
  multipliers = {"easy": 1.0, "medium": 2.0, "hard": 5.0}
826
  m = multipliers[self._difficulty]
827
 
@@ -829,27 +842,37 @@ class CodeAssessmentEnvironment(Environment):
829
  reward = m
830
  if self._current_streak >= 3:
831
  reward += 0.5
832
- elif score > 0:
833
  reward = m * score
834
  if self._difficulty == "easy":
835
  reward *= 0.5
836
  else:
837
- reward = -0.3 if self._difficulty == "hard" else 0.0
838
  return reward
839
 
840
  # ------------------------------------------------------------------
841
- # Progression
842
  # ------------------------------------------------------------------
843
- def _advance(self):
844
- if self._problems_solved >= 8 and self._difficulty != "hard":
845
- self._difficulty = "hard"
846
- elif self._problems_solved >= 4 and self._difficulty == "easy":
847
- self._difficulty = "medium"
 
 
 
 
848
 
 
 
 
 
 
 
849
  pool = PROBLEMS[self._difficulty]
850
  candidates = [p for p in pool if id(p) not in self._used]
851
  if not candidates:
852
  self._used = set()
853
  candidates = pool
854
  self._current_problem = random.choice(candidates)
855
- self._used.add(id(self._current_problem))
 
597
  expected_output=None,
598
  feedback="Welcome! Evaluate the AI response and submit your judgment.",
599
  is_correct=False,
600
+ partial_credit=0.01,
601
  problems_solved=0,
602
  current_streak=0,
603
  done=False,
604
+ reward=0.01,
605
  )
606
 
607
  def step(self, action: CodeAssessmentAction) -> CodeAssessmentObservation: # type: ignore[override]
 
611
 
612
  is_correct, partial_credit, feedback = self._grade(task_type, action.answer, problem)
613
 
614
+ shaped_reward = self._calculate_reward(is_correct, partial_credit)
615
+ self._total_reward += shaped_reward
616
 
617
  if is_correct:
618
  self._problems_solved += 1
 
623
  done = self._state.step_count >= self.MAX_STEPS
624
  expected_str = self._format_expected(task_type, problem)
625
 
626
+ # Step-based progression: guarantee all 3 tasks are reached
627
+ self._update_difficulty()
628
+
629
  if is_correct:
630
+ self._pick_next_problem()
631
 
632
  next_task = TASK_TYPES[self._difficulty]
633
  p = self._current_problem
 
647
  problems_solved=self._problems_solved,
648
  current_streak=self._current_streak,
649
  done=done,
650
+ reward=partial_credit,
651
  metadata={
652
+ "shaped_reward": shaped_reward,
653
  "total_reward": self._total_reward,
654
  "step": self._state.step_count,
655
  "task_type": next_task,
 
674
  scores = problem["expected_scores"]
675
  return ", ".join(f"{k}={v}" for k, v in scores.items())
676
 
677
+ # ------------------------------------------------------------------
678
+ # Clamp score to strictly (0, 1) β€” validator rejects 0.0 and 1.0
679
+ # ------------------------------------------------------------------
680
+ @staticmethod
681
+ def _clamp(score: float) -> float:
682
+ return max(0.01, min(0.99, score))
683
+
684
  # ------------------------------------------------------------------
685
  # Grading dispatch
686
  # ------------------------------------------------------------------
687
  def _grade(self, task_type: str, answer: str, problem: Dict) -> Tuple[bool, float, str]:
688
  if task_type == "correctness_check":
689
+ is_correct, score, fb = self._grade_correctness(answer, problem)
690
  elif task_type == "tone_appropriateness":
691
+ is_correct, score, fb = self._grade_tone(answer, problem)
692
  else:
693
+ is_correct, score, fb = self._grade_multi_dimensional(answer, problem)
694
+ return is_correct, self._clamp(score), fb
695
 
696
  # ── Task 1: Correctness Check ─────────────────────────────────────
697
  def _grade_correctness(self, answer: str, problem: Dict) -> Tuple[bool, float, str]:
 
707
  r_match = expected_r in given_r or given_r in expected_r
708
 
709
  if j_match and r_match:
710
+ return True, 0.95, f"Correct! {problem['explanation']}"
711
  if j_match:
712
  return False, 0.6, f"Judgment correct, wrong reason. Expected reason: '{expected_r}'. {problem['explanation']}"
713
  if r_match:
 
716
  VALID = {"correct", "incorrect", "partially-correct"}
717
  if given_j in VALID:
718
  return False, 0.2, f"Wrong. Expected: '{expected_j}, {expected_r}'. {problem['explanation']}"
719
+ return False, 0.05, f"Invalid format. Expected: '{expected_j}, {expected_r}'. {problem['explanation']}"
720
 
721
  # ── Task 2: Tone & Audience Appropriateness ───────────────────────
722
  def _grade_tone(self, answer: str, problem: Dict) -> Tuple[bool, float, str]:
 
744
  # Score issues via F1
745
  if "none" in expected_issues:
746
  if found_issues <= {"none"} or not found_issues:
747
+ issues_score = 0.95
748
  else:
749
  found_issues.discard("none")
750
  issues_score = 0.2 # false positives
 
753
  tp = len(found_issues & expected_issues)
754
  fp = len(found_issues - expected_issues)
755
  fn = len(expected_issues - found_issues)
756
+ prec = tp / (tp + fp) if (tp + fp) else 0.05
757
+ rec = tp / (tp + fn) if (tp + fn) else 0.05
758
+ issues_score = (2 * prec * rec / (prec + rec)) if (prec + rec) else 0.05
759
 
760
  # Combined score: 50% rating + 50% issues
761
+ score = (0.45 if rating_match else 0.05) + 0.5 * issues_score
762
 
763
+ if rating_match and issues_score >= 0.9:
764
+ return True, 0.95, f"Correct! {problem['explanation']}"
765
 
766
  parts_fb = []
767
  if not rating_match:
 
789
 
790
  parsed_count = sum(1 for v in given.values() if v is not None)
791
  if parsed_count == 0:
792
+ return False, 0.05, (
793
  f"Could not parse scores. Expected format: correctness=N, tone=N, empathy=N, safety=N. "
794
  f"Expected: {self._format_expected('multi_dimensional', problem)}. "
795
  f"{problem['explanation']}"
 
802
  exp = expected[dim]
803
  got = given[dim]
804
  if got is None:
805
+ dim_scores[dim] = 0.05
806
  dim_feedback.append(f"{dim}: missing (expected {exp})")
807
  continue
808
 
809
  diff = abs(exp - got)
810
  if diff <= 1:
811
+ dim_scores[dim] = 0.95
812
  elif diff <= 2:
813
  dim_scores[dim] = 0.7
814
  elif diff <= 3:
815
  dim_scores[dim] = 0.4
816
  else:
817
+ dim_scores[dim] = max(0.05, 0.95 - diff / 10.0)
818
 
819
  if diff > 1:
820
  dim_feedback.append(f"{dim}: gave {got}, expected {exp} (off by {diff})")
821
 
822
  overall = sum(dim_scores.values()) / 4.0
823
+ all_close = all(s >= 0.9 for s in dim_scores.values())
824
 
825
  if all_close:
826
+ return True, 0.95, f"Excellent! All dimensions within Β±1. {problem['explanation']}"
827
 
828
  detail = ". ".join(dim_feedback) if dim_feedback else "Close on all dimensions"
829
+ return False, round(max(0.05, min(0.95, overall)), 2), (
830
  f"Score: {overall:.0%}. {detail}. {problem['explanation']}"
831
  )
832
 
 
834
  # Reward
835
  # ------------------------------------------------------------------
836
  def _calculate_reward(self, is_correct: bool, score: float) -> float:
837
+ """Shaped reward β€” stored in metadata, not in observation.reward."""
838
  multipliers = {"easy": 1.0, "medium": 2.0, "hard": 5.0}
839
  m = multipliers[self._difficulty]
840
 
 
842
  reward = m
843
  if self._current_streak >= 3:
844
  reward += 0.5
845
+ elif score > 0.1:
846
  reward = m * score
847
  if self._difficulty == "easy":
848
  reward *= 0.5
849
  else:
850
+ reward = 0.05
851
  return reward
852
 
853
  # ------------------------------------------------------------------
854
+ # Progression (step-based β€” guarantees all 3 tasks are reached)
855
  # ------------------------------------------------------------------
856
+ def _update_difficulty(self):
857
+ """Switch task based on step count so all 3 tasks are always exercised."""
858
+ step = self._state.step_count
859
+ if step <= 5:
860
+ new_diff = "easy"
861
+ elif step <= 10:
862
+ new_diff = "medium"
863
+ else:
864
+ new_diff = "hard"
865
 
866
+ if new_diff != self._difficulty:
867
+ self._difficulty = new_diff
868
+ self._pick_next_problem()
869
+
870
+ def _pick_next_problem(self):
871
+ """Select a new problem from the current difficulty, avoiding repeats."""
872
  pool = PROBLEMS[self._difficulty]
873
  candidates = [p for p in pool if id(p) not in self._used]
874
  if not candidates:
875
  self._used = set()
876
  candidates = pool
877
  self._current_problem = random.choice(candidates)
878
+ self._used.add(id(self._current_problem))