Skip to content

Commit 8a409d7

Browse files
Merge pull request #193 from EvanProgramming/codex/issue-192-deepseek-xml-actions
fix: parse DeepSeek XML action blocks
2 parents 7567b4d + f33a114 commit 8a409d7

3 files changed

Lines changed: 42 additions & 9 deletions

File tree

‎main.py‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3679,6 +3679,21 @@ def _add(data: dict) -> None:
36793679
if args:
36803680
_add({"action": TOOL_ALIASES.get(raw_action, raw_action), "args": args})
36813681

3682+
xml_action_pattern = re.compile(
3683+
r"<\s*action\s*>\s*(?P<name>[A-Za-z_]\w*)[ \t]*\r?\n"
3684+
r"(?P<args>[\s\S]*?)</\s*action\s*>",
3685+
re.IGNORECASE,
3686+
)
3687+
xml_actions = list(xml_action_pattern.finditer(text))
3688+
# Provider XML is accepted only as one complete, unambiguous action block.
3689+
if len(xml_actions) == 1 and not re.search(
3690+
r"<\s*action\s*>[\s\S]*<\s*action\s*>", text, re.IGNORECASE):
3691+
xml_action = xml_actions[0]
3692+
raw_action = xml_action.group("name").strip().lower()
3693+
args = html.unescape(xml_action.group("args").strip())
3694+
if args and _is_valid_action(raw_action):
3695+
_add({"action": TOOL_ALIASES.get(raw_action, raw_action), "args": args})
3696+
36823697
for data in _collect_unwrapped_tool_calls(text)[0]:
36833698
if _is_valid_action(data.get("action")):
36843699
_add(data)
@@ -4378,15 +4393,15 @@ class DeepSeekDSMLFilter:
43784393
r"(?i)(?<![\w])(?P<kind>Action|Thought|Plan|TaskList|TaskDone|DefineTool)\s*:"
43794394
)
43804395
_GENERIC_OPEN_RE = re.compile(
4381-
r"<\s*(?P<kind>invoke|parameter|calls|tool_calls|function_calls|tool_use|notes|thought|reasoning)\b[^>]*>",
4396+
r"<\s*(?P<kind>action|invoke|parameter|calls|tool_calls|function_calls|tool_use|notes|thought|reasoning)\b[^>]*>",
43824397
re.IGNORECASE,
43834398
)
43844399
_GENERIC_CLOSE_RE = re.compile(
4385-
r"</\s*(?P<kind>invoke|parameter|calls|tool_calls|function_calls|tool_use|notes|thought|reasoning)\s*>",
4400+
r"</\s*(?P<kind>action|invoke|parameter|calls|tool_calls|function_calls|tool_use|notes|thought|reasoning)\s*>",
43864401
re.IGNORECASE,
43874402
)
43884403
_GENERIC_KINDS = (
4389-
"invoke", "parameter", "calls", "tool_calls", "function_calls",
4404+
"action", "invoke", "parameter", "calls", "tool_calls", "function_calls",
43904405
"tool_use", "notes", "thought", "reasoning",
43914406
)
43924407
_ACTION_MARKER_RE = _ACTION_MARKER_RE
@@ -4520,7 +4535,7 @@ def _generic_block_end(cls, value: str, match: re.Match[str]) -> int | None:
45204535
kind = match.group("kind").lower()
45214536
close_kinds = (
45224537
"parameter" if kind == "parameter" else
4523-
"invoke|calls|tool_calls|function_calls|tool_use|notes|thought|reasoning"
4538+
"action|invoke|calls|tool_calls|function_calls|tool_use|notes|thought|reasoning"
45244539
)
45254540
close = re.compile(rf"</\s*(?:{close_kinds})\s*>", re.IGNORECASE).search(value, match.end())
45264541
return close.end() if close else None

‎tests/test_streaming.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -392,11 +392,12 @@ def text(item):
392392
def test_generic_invoke_tags_split_at_every_boundary_stay_out_of_sse(self):
393393
marker = _SplitGenericInvokeProvider.marker
394394
expected = "Visible progress. after marker."
395-
for split in range(1, len(marker)):
396-
stream_filter = main.DeepSeekDSMLFilter()
397-
content = stream_filter.feed("Visible progress. " + marker[:split])
398-
content += stream_filter.feed(marker[split:] + " after marker.", final=True)
399-
self.assertEqual(content, expected, split)
395+
for control in (marker, '<action>\nrun_cmd\ncurl -w "HTTP_%{http_code}" /\n</action>'):
396+
for split in range(1, len(control)):
397+
stream_filter = main.DeepSeekDSMLFilter()
398+
content = stream_filter.feed("Visible progress. " + control[:split])
399+
content += stream_filter.feed(control[split:] + " after marker.", final=True)
400+
self.assertEqual(content, expected, (control, split))
400401

401402
session_id = "stream-generic-invoke-regression"
402403
provider = _SplitGenericInvokeProvider()

‎tests/test_task_consistency.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,23 @@ def test_unwrapped_provider_aliases_are_bounded_and_canonicalized(self):
753753
'</||DSML|| parameter><||DSML|| parameter name="path">x</||DSML|| parameter>',
754754
)), [])
755755

756+
xml_action = (
757+
'<action>\nexecute_terminal_command\n'
758+
'curl -s -w "HTTP_%{http_code}" "http://127.0.0.1:8765/?a=1&amp;b=2"\n'
759+
'</action>'
760+
)
761+
self.assertEqual(main._collect_tool_calls(xml_action), [{
762+
"action": "execute_terminal_command",
763+
"args": 'curl -s -w "HTTP_%{http_code}" "http://127.0.0.1:8765/?a=1&b=2"',
764+
}])
765+
self.assertEqual(main._clean_final_response(xml_action), "")
766+
self.assertEqual(main._collect_tool_calls('<action>\nunknown_tool\ntrue\n</action>'), [])
767+
self.assertEqual(main._collect_tool_calls('<action>\nrun_cmd\n\n</action>'), [])
768+
self.assertEqual(main._collect_tool_calls(xml_action + xml_action), [])
769+
self.assertEqual(main._collect_tool_calls(
770+
'<action>\nrun_cmd\n<action>\nrun_cmd\ntrue\n</action>\n</action>'
771+
), [])
772+
756773
malformed = main._parse_model_response("run_cmd: ```bash\nprintf alias-ok")
757774
self.assertEqual(malformed["tool_calls"], [])
758775
self.assertIn("No tool was executed", malformed["protocol_error"])

0 commit comments

Comments
 (0)