Skip to content

Commit d8c40d1

Browse files
committed
Address Monty REPL review feedback
1 parent f6491ef commit d8c40d1

3 files changed

Lines changed: 104 additions & 10 deletions

File tree

‎rlm/environments/monty_repl.py‎

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,43 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None:
119119
self.generic_visit(node)
120120
self.scope_depth -= 1
121121

122+
def visit_NamedExpr(self, node: ast.NamedExpr) -> None:
123+
if self.scope_depth == 0:
124+
self.add_target(node.target)
125+
self.generic_visit(node)
126+
127+
def visit_Match(self, node: ast.Match) -> None:
128+
if self.scope_depth == 0:
129+
for case in node.cases:
130+
self.collect_match_pattern(case.pattern)
131+
self.generic_visit(node)
132+
133+
def collect_match_pattern(self, pattern: ast.pattern) -> None:
134+
if isinstance(pattern, ast.MatchAs):
135+
if pattern.name:
136+
self.names.add(pattern.name)
137+
if pattern.pattern is not None:
138+
self.collect_match_pattern(pattern.pattern)
139+
elif isinstance(pattern, ast.MatchStar):
140+
if pattern.name:
141+
self.names.add(pattern.name)
142+
elif isinstance(pattern, ast.MatchMapping):
143+
if pattern.rest:
144+
self.names.add(pattern.rest)
145+
for subpattern in pattern.patterns:
146+
self.collect_match_pattern(subpattern)
147+
elif isinstance(pattern, ast.MatchSequence):
148+
for subpattern in pattern.patterns:
149+
self.collect_match_pattern(subpattern)
150+
elif isinstance(pattern, ast.MatchClass):
151+
for subpattern in pattern.patterns:
152+
self.collect_match_pattern(subpattern)
153+
for subpattern in pattern.kwd_patterns:
154+
self.collect_match_pattern(subpattern)
155+
elif isinstance(pattern, ast.MatchOr):
156+
for subpattern in pattern.patterns:
157+
self.collect_match_pattern(subpattern)
158+
122159

123160
class MontyREPL(NonIsolatedEnv):
124161
"""
@@ -149,6 +186,7 @@ def __init__(
149186
self.locals: dict[str, Any] = {}
150187
self.pending_llm_calls: list[RLMChatCompletion] = []
151188
self.stdout_parts: list[str] = []
189+
self.stderr_parts: list[str] = []
152190
self._context_count = 0
153191
self._history_count = 0
154192

@@ -165,6 +203,7 @@ def setup(self) -> None:
165203
self.locals = {}
166204
self.pending_llm_calls = []
167205
self.stdout_parts = []
206+
self.stderr_parts = []
168207
self._context_count = 0
169208
self._history_count = 0
170209

@@ -176,13 +215,12 @@ def execute_code(self, code: str) -> REPLResult:
176215
"""Execute code in the Monty sandbox and return result."""
177216
start_time = time.perf_counter()
178217
self.stdout_parts = []
218+
self.stderr_parts = []
179219
self.pending_llm_calls = []
180220

181221
wrapper_script = self.build_wrapper_script(code)
182222
external_functions = {
183223
"__rlm_capture_locals": self.capture_locals,
184-
"FINAL_VAR": self.final_var,
185-
"SHOW_VARS": self.show_vars,
186224
"llm_query": self.llm_query,
187225
"llm_query_batched": self.llm_query_batched,
188226
}
@@ -204,15 +242,20 @@ def execute_code(self, code: str) -> REPLResult:
204242

205243
return REPLResult(
206244
stdout="".join(self.stdout_parts),
207-
stderr="",
245+
stderr="".join(self.stderr_parts),
208246
locals=self.locals.copy(),
209247
execution_time=time.perf_counter() - start_time,
210248
rlm_calls=self.pending_llm_calls.copy(),
211249
)
212250
except Exception as exc:
251+
stderr = "".join(self.stderr_parts)
252+
if stderr:
253+
stderr = f"{stderr.rstrip()}\n{type(exc).__name__}: {exc}"
254+
else:
255+
stderr = f"{type(exc).__name__}: {exc}"
213256
return REPLResult(
214257
stdout="".join(self.stdout_parts),
215-
stderr=f"{type(exc).__name__}: {exc}",
258+
stderr=stderr,
216259
locals=self.locals.copy(),
217260
execution_time=time.perf_counter() - start_time,
218261
rlm_calls=self.pending_llm_calls.copy(),
@@ -221,18 +264,56 @@ def execute_code(self, code: str) -> REPLResult:
221264
def build_wrapper_script(self, user_code: str) -> str:
222265
"""Build a wrapper script that restores state, captures output, and persists locals."""
223266
lines: list[str] = []
224-
for key in self.locals:
225-
if key.isidentifier():
226-
lines.append(f"{key} = __rlm_state.get({key!r})")
227-
228-
lines.append(user_code)
229-
230267
assigned_names = self.collect_assigned_names(user_code)
231268
persisted_names = {
232269
name
233270
for name in set(self.locals) | assigned_names
234271
if name.isidentifier() and not name.startswith("_") and name not in RESERVED_NAMES
235272
}
273+
for name in sorted(persisted_names):
274+
lines.append(f"{name} = __rlm_state.get({name!r})")
275+
276+
lines.append("def FINAL_VAR(variable_name):")
277+
lines.append(' variable_name = variable_name.strip().strip("\\"\'")')
278+
for name in sorted(persisted_names):
279+
lines.append(f" if variable_name == {name!r}:")
280+
lines.append(" try:")
281+
lines.append(f" return str({name})")
282+
lines.append(" except NameError:")
283+
lines.append(" pass")
284+
lines.append(" available = []")
285+
for name in sorted(persisted_names):
286+
lines.append(" try:")
287+
lines.append(f" {name}")
288+
lines.append(f" available.append({name!r})")
289+
lines.append(" except NameError:")
290+
lines.append(" pass")
291+
lines.append(" if available:")
292+
lines.append(
293+
" return f\"Error: Variable '{variable_name}' not found. "
294+
"Available variables: {available}. "
295+
'You must create and assign a variable BEFORE calling FINAL_VAR on it."'
296+
)
297+
lines.append(
298+
" return f\"Error: Variable '{variable_name}' not found. "
299+
"No variables have been created yet. "
300+
'You must create and assign a variable in a REPL block BEFORE calling FINAL_VAR on it."'
301+
)
302+
303+
lines.append("def SHOW_VARS():")
304+
lines.append(" available = {}")
305+
for name in sorted(persisted_names):
306+
lines.append(" try:")
307+
lines.append(f" available[{name!r}] = type({name}).__name__")
308+
lines.append(" except NameError:")
309+
lines.append(" pass")
310+
lines.append(" if not available:")
311+
lines.append(
312+
' return "No variables created yet. Use ```repl``` blocks to create variables."'
313+
)
314+
lines.append(' return f"Available variables: {available}"')
315+
316+
lines.append(user_code)
236317

237318
lines.append("__rlm_state_out = {}")
238319
for name in sorted(persisted_names):
@@ -248,6 +329,8 @@ def handle_print_callback(self, stream: Literal["stdout", "stderr"], text: str)
248329
"""Collect printed output from Monty."""
249330
if stream == "stdout":
250331
self.stdout_parts.append(text)
332+
elif stream == "stderr":
333+
self.stderr_parts.append(text)
251334

252335
def capture_locals(self, vars_dict: dict[str, Any]) -> None:
253336
"""Capture locals after execution."""

‎tests/test_imports.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,4 +517,5 @@ def test_all_environment_classes_importable(self):
517517

518518
assert isinstance(MontyREPL, type)
519519
except Exception:
520+
# Optional dependency may not be available in CI.
520521
pass

‎tests/test_monty_repl.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ def test_final_var(self):
2929
result = repl.execute_code("print(FINAL_VAR('x'))")
3030
assert "10" in result.stdout
3131

32+
def test_final_var_same_block(self):
33+
repl = MontyREPL()
34+
result = repl.execute_code("x = 3\nprint(FINAL_VAR('x'))")
35+
assert "3" in result.stdout
36+
37+
def test_stderr_capture(self):
38+
repl = MontyREPL()
39+
repl.handle_print_callback("stderr", "oops")
40+
assert "oops" in repl.stderr_parts
41+
3242
def test_context_and_history_counts(self):
3343
repl = MontyREPL()
3444
assert repl.get_context_count() == 0

0 commit comments

Comments
 (0)