Skip to content

Commit dea6c86

Browse files
Parse XML tool-use repl blocks in find_code_blocks
Claude models often emit native XML tool-use syntax (<invoke name="repl"><parameter name="code">...</parameter></invoke>) instead of ```repl``` fences, so none of their code was executed. Fall back to parsing those blocks when no fenced block is present. Fixes #179
1 parent d04208a commit dea6c86

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

‎rlm/utils/parsing.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
def find_code_blocks(text: str) -> list[str]:
1111
"""
1212
Find REPL code blocks in text wrapped in triple backticks and return List of content(s).
13-
Returns None if no code blocks are found.
13+
If there are none, fall back to XML tool-use blocks (`<invoke name="repl">`), which
14+
Claude models often emit instead of fences. Returns an empty list if nothing is found.
1415
"""
1516
pattern = r"```repl\s*\n(.*?)\n```"
1617
results = []
@@ -19,6 +20,11 @@ def find_code_blocks(text: str) -> list[str]:
1920
code_content = match.group(1).strip()
2021
results.append(code_content)
2122

23+
if not results:
24+
xml_pattern = r'<invoke\s+name="repl">\s*<parameter\s+name="code">(.*?)</parameter>'
25+
for match in re.finditer(xml_pattern, text, re.DOTALL):
26+
results.append(match.group(1).strip())
27+
2228
return results
2329

2430

‎tests/test_parsing.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,52 @@ def factorial(n):
7474
assert "def factorial(n):" in blocks[0]
7575
assert "return n * factorial(n - 1)" in blocks[0]
7676

77+
def test_xml_tool_call_block(self):
78+
"""Claude models may emit native XML tool-use syntax instead of ```repl``` fences."""
79+
text = """I'll inspect the context first.
80+
81+
<function_calls>
82+
<invoke name="repl">
83+
<parameter name="code">
84+
print("Length of context:", len(context))
85+
print(context[:500])
86+
</parameter>
87+
</invoke>
88+
</function_calls>"""
89+
blocks = find_code_blocks(text)
90+
assert blocks == ['print("Length of context:", len(context))\nprint(context[:500])']
91+
92+
def test_multiple_xml_tool_call_blocks(self):
93+
text = """<function_calls>
94+
<invoke name="repl">
95+
<parameter name="code">a = 1</parameter>
96+
</invoke>
97+
<invoke name="repl">
98+
<parameter name="code">
99+
b = a + 1
100+
</parameter>
101+
</invoke>
102+
</function_calls>"""
103+
blocks = find_code_blocks(text)
104+
assert blocks == ["a = 1", "b = a + 1"]
105+
106+
def test_xml_tool_call_for_other_tool_ignored(self):
107+
text = """<function_calls>
108+
<invoke name="search">
109+
<parameter name="code">x = 1</parameter>
110+
</invoke>
111+
</function_calls>"""
112+
assert find_code_blocks(text) == []
113+
114+
def test_fenced_blocks_take_precedence_over_xml(self):
115+
text = """```repl
116+
y = 2
117+
```
118+
<invoke name="repl">
119+
<parameter name="code">x = 1</parameter>
120+
</invoke>"""
121+
assert find_code_blocks(text) == ["y = 2"]
122+
77123

78124
class TestAnswerDictFinalAnswer:
79125
"""Tests for the ``answer`` dict completion signal surfaced via REPLResult.final_answer."""

0 commit comments

Comments
 (0)