Skip to content

Commit 5ea4efc

Browse files
liuzhengyang699codelion
authored andcommitted
fix(rstar): preserve signed and decimal answers during search
1 parent c73155e commit 5ea4efc

2 files changed

Lines changed: 55 additions & 7 deletions

File tree

‎optillm/rstar.py‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -317,13 +317,15 @@ def evaluate(self, node: Node) -> float:
317317

318318
def extract_answer(self, final_state: str) -> Tuple[str, float]:
319319
logger.debug(f"Extracting answer from state: {final_state}")
320+
# Keep the full numeric value when assigning rewards and counting votes.
321+
number_pattern = r"[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?"
320322
patterns = [
321-
r"The answer is (\d+)",
322-
r"The final answer is (\d+)",
323-
r"Therefore, the answer is (\d+)",
324-
r"So, the answer is (\d+)",
325-
r"Thus, the answer is (\d+)",
326-
r"In conclusion, the answer is (\d+)",
323+
rf"The answer is ({number_pattern})",
324+
rf"The final answer is ({number_pattern})",
325+
rf"Therefore, the answer is ({number_pattern})",
326+
rf"So, the answer is ({number_pattern})",
327+
rf"Thus, the answer is ({number_pattern})",
328+
rf"In conclusion, the answer is ({number_pattern})",
327329
]
328330

329331
for pattern in patterns:
@@ -335,7 +337,7 @@ def extract_answer(self, final_state: str) -> Tuple[str, float]:
335337
return answer, confidence
336338

337339
# If no pattern is found, try to extract any number
338-
numbers = re.findall(r'\d+', final_state)
340+
numbers = re.findall(number_pattern, final_state)
339341
if numbers:
340342
answer = numbers[-1] # Take the last number found
341343
confidence = 0.5 # Lower confidence as it's not in the expected format
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import unittest
2+
from unittest.mock import Mock
3+
4+
from optillm.rstar import Node, RStar
5+
6+
7+
class TestRStarNumericAnswers(unittest.TestCase):
8+
def setUp(self):
9+
self.search = RStar("", Mock(), "mock", max_depth=1, num_rollouts=1)
10+
11+
def test_explicit_answers_keep_the_complete_number(self):
12+
for answer in ("-3", "+3", "1.25", "-0.25", ".5", "-.5", "1.2e-3", "-2E+4", "42"):
13+
for prefix in ("The answer is", "The final answer is", "Thus, the answer is"):
14+
with self.subTest(answer=answer, prefix=prefix):
15+
self.assertEqual(
16+
self.search.extract_answer(f"{prefix} {answer}."),
17+
(answer, 1.0),
18+
)
19+
20+
def test_fallback_keeps_sign_decimal_and_exponent(self):
21+
for answer in ("-3", "1.25", "-.5", "1.2e-3", "42"):
22+
with self.subTest(answer=answer):
23+
self.assertEqual(
24+
self.search.extract_answer(f"After 2 steps, we obtain {answer}."),
25+
(answer, 0.5),
26+
)
27+
28+
def test_answer_without_numbers_remains_unknown(self):
29+
self.assertEqual(self.search.extract_answer("More reasoning is needed."), ("", 0.0))
30+
31+
def test_signed_answer_receives_explicit_answer_reward(self):
32+
node = Node("The final answer is -1.25", "A1")
33+
self.assertEqual(self.search.evaluate(node), 1.0)
34+
35+
def test_answer_voting_does_not_merge_different_decimal_values(self):
36+
states = ["The answer is 1.25", "The answer is 1.75", "The answer is 1.75"]
37+
answers = [self.search.extract_answer(state) for state in states]
38+
self.assertEqual(self.search.select_best_answer(answers), "1.75")
39+
40+
def test_solve_preserves_the_generated_numeric_answer(self):
41+
self.search.generate_response = Mock(return_value="The final answer is -1.25")
42+
43+
answer, tokens = self.search.solve("Compute the result.")
44+
45+
self.assertEqual(answer, "-1.25")
46+
self.assertEqual(tokens, 0)

0 commit comments

Comments
 (0)