Spaces:
Sleeping
Sleeping
Update server/code_assessment_environment.py
#15
by rsaibhargav - opened
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.
|
| 601 |
problems_solved=0,
|
| 602 |
current_streak=0,
|
| 603 |
done=False,
|
| 604 |
-
reward=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 |
-
|
| 615 |
-
self._total_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.
|
| 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=
|
| 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 |
-
|
| 679 |
elif task_type == "tone_appropriateness":
|
| 680 |
-
|
| 681 |
else:
|
| 682 |
-
|
|
|
|
| 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,
|
| 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.
|
| 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 =
|
| 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.
|
| 745 |
-
rec = tp / (tp + fn) if (tp + fn) else 0.
|
| 746 |
-
issues_score = (2 * prec * rec / (prec + rec)) if (prec + rec) else 0.
|
| 747 |
|
| 748 |
# Combined score: 50% rating + 50% issues
|
| 749 |
-
score = (0.
|
| 750 |
|
| 751 |
-
if rating_match and issues_score >= 0.
|
| 752 |
-
return True,
|
| 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.
|
| 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.
|
| 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] =
|
| 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.
|
| 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 >=
|
| 812 |
|
| 813 |
if all_close:
|
| 814 |
-
return True,
|
| 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 =
|
| 838 |
return reward
|
| 839 |
|
| 840 |
# ------------------------------------------------------------------
|
| 841 |
-
# Progression
|
| 842 |
# ------------------------------------------------------------------
|
| 843 |
-
def
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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))
|