Skip to content

Commit 7468f58

Browse files
authored
fix(repl_env): handle nested FINAL parens
Handle nested parentheses in FINAL(...) extraction and keep REPL runner and server extraction behavior aligned.
1 parent b5e1395 commit 7468f58

3 files changed

Lines changed: 50 additions & 8 deletions

File tree

envs/repl_env/runner.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,18 @@ def _default_answer(
243243
]
244244
try:
245245
response = self._chat(final_prompt, model)
246-
# Try to extract FINAL(...) from the response
247-
match = re.search(r"FINAL\((.*?)\)", response, re.DOTALL)
248-
if match:
249-
return match.group(1).strip()
246+
# Try to extract FINAL(...) from the response. Paren-counting handles
247+
# nested parens and mid-sentence FINAL without regex flag trade-offs.
248+
idx = response.find("FINAL(")
249+
if idx != -1:
250+
depth, start = 0, idx + len("FINAL")
251+
for i, ch in enumerate(response[idx + len("FINAL"):], start=idx + len("FINAL")):
252+
if ch == "(":
253+
depth += 1
254+
elif ch == ")":
255+
depth -= 1
256+
if depth == 0:
257+
return response[start + 1 : i].strip()
250258
# If no FINAL pattern, return the raw response as best-effort
251259
return response.strip() if response.strip() else None
252260
except Exception:

envs/repl_env/server/repl_environment.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -536,10 +536,18 @@ def _extract_final_answer(self, stdout: str) -> Optional[str]:
536536
Returns:
537537
Final answer string or None if not found
538538
"""
539-
# Pattern 1: RLM-style FINAL(answer)
540-
final_match = re.search(r"FINAL\((.*?)\)", stdout, re.DOTALL)
541-
if final_match:
542-
return final_match.group(1).strip()
539+
# Pattern 1: RLM-style FINAL(answer). Paren-counting handles nested
540+
# parens (e.g. FINAL(f(x))) and multi-line values without regex flag trade-offs.
541+
idx = stdout.find("FINAL(")
542+
if idx != -1:
543+
depth, start = 0, idx + len("FINAL")
544+
for i, ch in enumerate(stdout[idx + len("FINAL"):], start=idx + len("FINAL")):
545+
if ch == "(":
546+
depth += 1
547+
elif ch == ")":
548+
depth -= 1
549+
if depth == 0:
550+
return stdout[start + 1 : i].strip()
543551

544552
# Pattern 2: RLM-style FINAL_VAR(variable_name)
545553
final_var_match = re.search(r"FINAL_VAR\((\w+)\)", stdout)

tests/envs/test_repl_env.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,32 @@ def test_final_pattern_basic(self):
229229
assert obs.done
230230
assert obs.metadata["final_answer"] == "42"
231231

232+
@pytest.mark.parametrize(
233+
"code, expected",
234+
[
235+
# Nested function calls inside FINAL(...).
236+
("print('FINAL(f(x))')", "f(x)"),
237+
# Tuple as the final answer.
238+
("print('FINAL((1, 2, 3))')", "(1, 2, 3)"),
239+
# Math expression with multiple nested parens (e2b_repl_example).
240+
(
241+
"print('FINAL(2^(2^(2^(2))) = 65536)')",
242+
"2^(2^(2^(2))) = 65536",
243+
),
244+
# Dict containing a tuple value.
245+
("print(\"FINAL({'a': (1, 2)})\")", "{'a': (1, 2)}"),
246+
# Output after FINAL must not bleed into the extracted answer.
247+
("print('FINAL(42)\\nresult: (ok)')", "42"),
248+
],
249+
)
250+
def test_final_pattern_nested_parentheses(self, code, expected):
251+
"""FINAL(...) extraction must handle nested parentheses (rlm #75)."""
252+
env = REPLEnvironment()
253+
env.reset()
254+
obs = env.step(REPLAction(code=code))
255+
assert obs.done
256+
assert obs.metadata["final_answer"] == expected
257+
232258
def test_final_var_pattern(self):
233259
"""Test FINAL_VAR() pattern."""
234260
env = REPLEnvironment()

0 commit comments

Comments
 (0)